T12, T13: Sharkey and Akkoma in the pasture, and two bugs Akkoma found
Build / Build (push) Successful in 4m51s
Deploy / privapub.thepra.dev (push) Successful in 5m2s

Sharkey 2025.4.7 is the Misskey peer under another name, image, database, Redis db and
home. Its scenario is Misskey's plus edits both ways and the FEP-e232 quote tag: 40 checks
pass.

Akkoma 3.20.1 publishes no image, so tools/pasture/images/akkoma installs its OTP
release, pinned by checksum, and appends Caddy's CA to the CA bundles the release ships.
Its scenario has 44 checks, which pass three runs in a row:
- follows, posts, CW, followers-only posts and the published time;
- replies, likes, boosts and their undos;
- EmojiReact both ways and withdrawn;
- DMs, polls, quotes, media, edits with history and deletes, both ways;
- unfollow, block, unblock and statistics.

The two bugs, both fixed:
- Followers-only posts arrived as DMs on Pleroma and Akkoma. They call a post private
  only if an address in `to` contains "/followers" or its cc is not empty. Ours is
  /groupies, and a post mentioning nobody had an empty cc. The followers collection is now
  named in cc as well, which tells nobody anything new.
- Akkoma's open polls showed as ended and refused votes. Akkoma carries an open poll's
  end in `closed` and sends no `endTime`. A `closed` in the future is now read as the end.

Neither of these is a PrivaPub bug:
- Akkoma's Linkify never takes @user@host.test for a mention, so its DM addresses alice
  with to[].
- Its API never reports a remote blocker as blocked_by, so the block is read from its
  database.

Also in this commit:
- The rate limits are configuration (RateLimits: AccountsPerMinute, InboxBurst,
  InboxPerTenSeconds), with the old values as defaults. The pasture raises the accounts
  limit, which back-to-back runs from one address had hit.
- A new Lemmy never sends what it queued for a server before it started that server's
  send worker, so the scenario waits for the worker before its first follow.

All six peers in one clean pass: GoToSocial 54 (+1 expected), Mastodon 49 (+2), Misskey
35, Sharkey 40, Akkoma 44, and Lemmy 20 (+3) once the worker wait was added. 642 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-03 15:24:08 +02:00
1 parent aa7e43a61e
commit ab526ed291
22 files changed
+361 -19

No files matched your search

+5
View File
@@ -32,3 +32,8 @@ lemmy.test {
tls internal
reverse_proxy pasture-lemmy:8536
}
akkoma.test {
tls internal
reverse_proxy pasture-akkoma:4000
}
+1
View File
@@ -25,6 +25,7 @@
"AcceptAnyCertificate": true
},
"Media": { "Root": "/tmp/privapub-media" },
"RateLimits": { "AccountsPerMinute": 1000 },
"Kestrel": { "Endpoints": { "Http": { "Url": "http://0.0.0.0:80", "Protocols": "Http1AndHttp2" } } },
"Serilog": {
"MinimumLevel": { "Default": "Information", "Override": { "Microsoft": "Warning", "System": "Warning" } },
+16
View File
@@ -0,0 +1,16 @@
# Akkoma publishes no image, so this is its OTP release on the Ubuntu it is built for. The "stable" zip moves; the
# checksum pins 3.20.1 and fails the build when stable has moved on.
FROM docker.io/library/ubuntu:22.04
ARG AKKOMA_ZIP=https://akkoma-updates.s3-website.fr-par.scw.cloud/stable/akkoma-amd64.zip
ARG AKKOMA_SHA256=a06d1077dc43357923ffbebbee509f1faa2d66ae6078828ad35f9ad34ec6f96c
RUN apt-get update \
&& DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
ca-certificates curl unzip libncurses5 libmagic1 libmagic-mgc file postgresql-client \
&& rm -rf /var/lib/apt/lists/*
RUN curl -fsSL "$AKKOMA_ZIP" -o /tmp/akkoma.zip \
&& echo "$AKKOMA_SHA256 /tmp/akkoma.zip" | sha256sum -c - \
&& unzip -q /tmp/akkoma.zip -d /tmp && mv /tmp/release /opt/akkoma && rm /tmp/akkoma.zip \
&& mkdir -p /var/lib/akkoma/uploads /var/lib/akkoma/static /etc/akkoma
ENV AKKOMA_CONFIG_PATH=/etc/akkoma/config.exs PLEROMA_CONFIG_PATH=/etc/akkoma/config.exs LANG=C.UTF-8 ELIXIR_ERL_OPTIONS=+fnu
COPY entrypoint.sh /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"]
+19
View File
@@ -0,0 +1,19 @@
#!/bin/sh
# Trusts the pasture's CA, writes the config on first start, migrates, and runs Akkoma in the foreground.
set -e
cp /pasture/ca/root.crt /usr/local/share/ca-certificates/pasture.crt && update-ca-certificates >/dev/null
# Mint and hackney read the bundles shipped in the release (CAStore, certifi), never the system's
for bundle in /opt/akkoma/lib/castore-*/priv/cacerts.pem /opt/akkoma/lib/certifi-*/priv/cacerts.pem; do
grep -q "pasture" "$bundle" || { echo "# pasture"; cat /pasture/ca/root.crt; } >> "$bundle"
done
if [ ! -f /etc/akkoma/config.exs ]; then
# pleroma_ctl passes its arguments on unquoted, so no value may contain a space
env -u AKKOMA_CONFIG_PATH -u PLEROMA_CONFIG_PATH /opt/akkoma/bin/pleroma_ctl instance gen --force --output /etc/akkoma/config.exs --output-psql /tmp/setup.psql \
--domain akkoma.test --instance-name PastureAkkoma --admin-email admin@akkoma.test --notify-email admin@akkoma.test \
--dbhost postgres --dbname akkoma --dbuser pasture --dbpass pasture --rum N --indexable N --db-configurable N \
--uploads-dir /var/lib/akkoma/uploads --static-dir /var/lib/akkoma/static --listen-ip 0.0.0.0 --listen-port 4000 \
--media-url https://akkoma.test/media --strip-uploads-metadata N --read-uploads-description Y --anonymize-uploads N \
</dev/null
fi
/opt/akkoma/bin/pleroma_ctl migrate
exec /opt/akkoma/bin/pleroma start
+1 -1
View File
@@ -1,7 +1,7 @@
#!/usr/bin/env bash
# Drives PrivaPub and each peer (started by run.sh) through each other's federation, as their users would, then checks
# that PrivaPub's statistics saw it all without naming anyone.
# usage: tools/pasture/interop.sh [peer...] peers: gts (default), mastodon, misskey, lemmy, crawler
# usage: tools/pasture/interop.sh [peer...] peers: gts (default), mastodon, misskey, sharkey, akkoma, lemmy, crawler
set -uo pipefail
here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
. "$here/lib/interop.sh"
+29
View File
@@ -0,0 +1,29 @@
# Akkoma 3.20.1: our own image (images/akkoma: the OTP release, pinned by checksum) on the shared Postgres. Its config
# is generated on first start; the scenario's user is made with pleroma_ctl and signs in with the password grant.
AKKOMA_IMAGE=${AKKOMA_IMAGE:-localhost/pasture-akkoma:3.20.1}
. "$here/peers/shared.sh"
akkoma_up() {
shared_postgres_up
podman image exists "$AKKOMA_IMAGE" || podman build -q -t "$AKKOMA_IMAGE" "$here/images/akkoma" >/dev/null
podman exec pasture-postgres sh -c "psql -U pasture -tc \"select 1 from pg_database where datname='akkoma'\" | grep -q 1 || createdb -U pasture akkoma"
podman exec pasture-postgres psql -U pasture -d akkoma -qc 'create extension if not exists citext; create extension if not exists pg_trgm; create extension if not exists "uuid-ossp";' >/dev/null
mkdir -p "$here/.state/akkoma"
podman run -d --replace --name pasture-akkoma --network $net \
-v "$here/.state/akkoma:/etc/akkoma:z" -v "$ca:/pasture/ca:z,ro" "$AKKOMA_IMAGE" >/dev/null
for _ in $(seq 1 120); do
site akkoma.test -s -o /dev/null -w '%{http_code}' https://akkoma.test:6443/api/v1/instance 2>/dev/null | grep -q 200 && break
sleep 3
done
podman exec pasture-akkoma /opt/akkoma/bin/pleroma_ctl user new akuser akuser@akkoma.test --password Akkoma-Pasture-Pass-1 \
--name akuser --admin --assume-yes >/dev/null 2>&1 || true
local app cid cs
app=$(site akkoma.test -s -X POST https://akkoma.test:6443/api/v1/apps -d 'client_name=pasture&redirect_uris=urn:ietf:wg:oauth:2.0:oob&scopes=read write follow push admin')
cid=$(echo "$app" | python3 -c "import sys,json; print(json.load(sys.stdin)['client_id'])" 2>/dev/null)
cs=$(echo "$app" | python3 -c "import sys,json; print(json.load(sys.stdin)['client_secret'])" 2>/dev/null)
site akkoma.test -s -X POST https://akkoma.test:6443/oauth/token --data-urlencode grant_type=password --data-urlencode username=akuser \
--data-urlencode password=Akkoma-Pasture-Pass-1 --data-urlencode "client_id=$cid" --data-urlencode "client_secret=$cs" \
--data-urlencode 'scope=read write follow push admin' \
| python3 -c "import sys,json; print(json.load(sys.stdin)['access_token'])" > "$here/.state/akkoma.token" 2>/dev/null || true
echo "akkoma: https://akkoma.test:6443"
}
+1 -1
View File
@@ -33,7 +33,7 @@ allowedPrivateNetworks:
- '192.168.0.0/16'
YML
podman run -d --replace --name pasture-$MISSKEY_NAME --network $net -e NODE_EXTRA_CA_CERTS=/pasture/ca/root.crt \
-v "$here/.state/$MISSKEY_NAME:/misskey/.config:z,ro" -v "$ca:/pasture/ca:z,ro" $MISSKEY_IMAGE >/dev/null
-v "$here/.state/$MISSKEY_NAME:${MISSKEY_HOME:-/misskey}/.config:z,ro" -v "$ca:/pasture/ca:z,ro" $MISSKEY_IMAGE >/dev/null
for _ in $(seq 1 120); do
site "$MISSKEY_NAME.test" -s -o /dev/null -w '%{http_code}' -X POST "https://$MISSKEY_NAME.test:6443/api/meta" -H 'Content-Type: application/json' -d '{}' 2>/dev/null | grep -q 200 && break
sleep 3
+8
View File
@@ -0,0 +1,8 @@
# Sharkey: the Misskey peer under its own name, image, database, Redis db and home. Its scenario is Misskey's plus what
# only Sharkey sends.
. "$here/peers/misskey.sh"
sharkey_up() {
MISSKEY_NAME=sharkey MISSKEY_IMAGE=${SHARKEY_IMAGE:-registry.activitypub.software/transfem-org/sharkey:2025.4.7} \
MISSKEY_REDIS_DB=3 MISSKEY_HOME=/sharkey misskey_up
}
+1 -1
View File
@@ -2,7 +2,7 @@
# A private test fediverse on one podman network: PrivaPub (privapub.test) and the peers asked for, behind one Caddy
# whose internal CA every side is told to accept (its root is copied to .ca/root.crt). From the workstation: PrivaPub's
# API at http://127.0.0.1:6971, every site at https://<name>.test:6443 (curl --resolve <name>.test:6443:127.0.0.1 -k).
# usage: tools/pasture/run.sh up [peer...] | down | logs <name> | ps peers: gts (default), mastodon, misskey, lemmy
# usage: tools/pasture/run.sh up [peer...] | down | logs <name> | ps peers: gts (default), mastodon, misskey, sharkey, akkoma, lemmy
set -euo pipefail
. "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/lib/pasture.sh"
+142
View File
@@ -0,0 +1,142 @@
# 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"
until_true 30 '[ "$(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&quote_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&quoted_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
+5
View File
@@ -13,6 +13,8 @@ lm() {
}
# the posts Lemmy holds in a community, and the one whose ap_id is given
lm_posts() { lm GET "post/list?community_id=$1&sort=new&limit=50" | j "print(json.dumps([p['post'] for p in (d.get('items') or d.get('posts') or [])]))"; }
# 1 once Lemmy runs a send worker for privapub.test
lm_worker() { podman exec pasture-postgres psql -U pasture -d lemmy -tAc "select count(*) from federation_queue_state q join instance i on i.id=q.instance_id where i.domain='privapub.test'"; }
lm_post_by_ap() { lm_posts "$1" | j "print(json.dumps(next((p for p in d if p['ap_id']=='$2'), None)))"; }
echo "lemmy"
@@ -30,6 +32,9 @@ dogs_gid=$(curl -s -X POST $P/clientapi/group/insert -H 'Content-Type: applicati
-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']['id'])")
[ -n "$dogs_on_lm" ] && ok "Lemmy resolves a PrivaPub community" || ko "Lemmy cannot resolve the PrivaPub community"
# Lemmy 1.0 starts its send worker for a newly seen server at the newest activity and never sends anything queued for
# that server before then, so on a clean pasture the first follow waits for the worker (up to a minute)
until_true 45 '[ "$(lm_worker)" = "1" ]' || true
lm POST community/follow "{\"community_id\":$dogs_on_lm,\"follow\":true}" >/dev/null
until_true 30 '[ "$(lm GET "community?id=$dogs_on_lm" | j "print(((d[\"community_view\"].get(\"community_actions\") or {}).get(\"follow_state\") or \"\").lower())")" = "accepted" ]' \
&& ok "lemmyuser follows the PrivaPub community (Accept arrived)" || ko "Lemmy's community follow not accepted"
+25
View File
@@ -0,0 +1,25 @@
# Sharkey 2025.4: Misskey's scenario under Sharkey's name, then what only Sharkey does: edits both ways, and quotes that
# carry a FEP-e232 Link tag instead of `quote`.
MISSKEY_NAME=sharkey
. "$here/scenarios/misskey.sh"
unset MISSKEY_NAME
echo " sharkey only"
# Misskey's scenario ends unfollowed and blocked, so follow again both ways
curl -s -o /dev/null -X POST -H "$KH" "$P/api/v1/accounts/$mk_on_p/follow"
mk following/create "{\"userId\":\"$alice_on_mk\"}" >/dev/null
until_true 30 '[ "$(mk_relation "$alice_on_mk" isFollowed)$(mk_relation "$alice_on_mk" isFollowing)" = "TrueTrue" ]' \
&& ok "alice and mkuser follow each other again" || ko "refollow after the block failed"
sk_note=$(mk notes/create '{"text":"before the edit","visibility":"public"}' | j "print(d['createdNote']['id'])")
until_true 30 'curl -s -H "$KH" "$P/api/v1/timelines/home" | grep -q "before the edit"' && ok "a Sharkey note reaches alice" || ko "Sharkey note missing on PrivaPub"
mk notes/edit "{\"editId\":\"$sk_note\",\"text\":\"after the edit\",\"visibility\":\"public\"}" >/dev/null
until_true 30 '[ "$(curl -s -H "$KH" "$P/api/v1/timelines/home" | j "print(any(\"after the edit\" in s[\"content\"] and s[\"edited_at\"] for s in d))")" = "True" ]' \
&& ok "mkuser's edit arrives as an edit" || ko "Sharkey edit not applied on PrivaPub"
k_edit=$(curl -s -X POST -H "$KH" $P/api/v1/statuses -d 'status=PrivaPub before the edit&visibility=public')
k_edit_id=$(echo "$k_edit" | j "print(d['id'])"); k_edit_uri=$(echo "$k_edit" | j "print(d['uri'])")
until_true 30 '[ "$(mk_note_by_uri "$alice_on_mk" "$k_edit_uri")" != "null" ]' || true
curl -s -o /dev/null -X PUT -H "$KH" "$P/api/v1/statuses/$k_edit_id" -d 'status=PrivaPub after the edit'
until_true 30 '[ "$(mk_note_by_uri "$alice_on_mk" "$k_edit_uri" | j "print(d and d[\"text\"])")" = "PrivaPub after the edit" ]' \
&& ok "alice's edit reaches Sharkey" || ko "PrivaPub edit not applied on Sharkey"
quote_tag=$(podman exec pasture-mongo mongosh --quiet PrivaPub --eval 'var p=db.Post.findOne({Text:/quoting PrivaPub/}, {}, {sort:{_id:-1}}); var r=p && db.ObjectRecord.findOne({PostId:p._id.toHexString()}); print(r ? /"type"\s*:\s*"Link"/.test(r.Raw) : "none")')
[ "$quote_tag" = "true" ] && ok "Sharkey's quote carries a FEP-e232 Link tag, and is understood" || ko "Sharkey's quote record has no Link tag ($quote_tag)"