Pasture: Vernissage 1.43.0

Vernissage joins the pasture on SQLite with its queues on the shared Redis; scenarios/vernissage.sh: follows both ways,
a photo with its alt text, likes, boosts and comments both ways, a deletion, the unfollow and statistics: 19 checks,
with no change to PrivaPub.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LsXgEaXee4GCU1hwYgPJXw
This commit is contained in:
thepraandClaude Opus 5.5 committed 2026-10-06 02:11:53 +02:00
1 parent 53158b4cf4
commit b218768904
6 files changed
+125

No files matched your search

+3
View File
@@ -561,6 +561,9 @@ tools/pasture/run.sh down # removes e
checks.
- **Pins (`scenarios/pins.sh`, needs mastodon):** a Mastodon account pins and unpins while alice follows it, alice pins
and unpins while it follows her, and a fresh account's earlier pin shows once PrivaPub resolves it. 8 checks.
- **Vernissage (1.43.0):** its image on SQLite in the `pasture-vernissage` volume, its queues on the shared Redis's
database 0 (it federates nothing without them; every other database is taken), its seeded admin (admin / admin)
logged in by the scenario. `scenarios/vernissage.sh`, 19 checks.
- **BookWyrm (0.9.3):** its image, migrated, on the shared Postgres (database `bookwyrm`) and Redis (databases 14
and 15), gunicorn and one Celery worker (federation runs there). It has no client API: bwuser (named
`bwuser@bookwyrm.test`, as its signup names users), a book and every status come from its Django shell
+1
View File
@@ -39,6 +39,7 @@ and every run starting clean, with signed fetches required (as privapub.thepra.d
- **Owncast 0.3.0**
- **Ghost 6.67** with its ActivityPub service 1.2.14
- **BookWyrm 0.9.3**
- **Vernissage 1.43.0**
- **Activity-Relay 2.0.9** and **aode-relay 0.3.129**, as relays PrivaPub reads from
- in the town only (a seeded community checked server by server): **Hollo 0.9.19**, **Iceshrimp.NET 2026.1.2-beta**,
**Pleroma 2.10.2**
+10
View File
@@ -753,6 +753,16 @@ PeerTube's own instance account announces each new video too, which we drop: nob
follow only PeerTube-like channels and accounts, never a persona. It checks the `Host` header against its own name,
without a port, before it gives out its OAuth client.
### Vernissage 1.43.0
- Photo sharing: a top-level post carries its photos (`Image`, with alt text, and EXIF in its API); comments are plain
replies. It names a PNG `image/jpeg` in its attachment.
- It federates only with its queues on (Redis); its API answers 400 to an attachment's metadata unless the whole
attachment comes back with it, and takes a new status from the same account only every 40 seconds or so.
- **Pasture evidence (2026-10-05, `tools/pasture/scenarios/vernissage.sh`):** 19 checks pass, with no change to
PrivaPub: follows both ways; a photo with its alt text reaches alice; her like, boost and comment land there; her
photo reaches the admin's home and its like and comment come back; a deletion; the unfollow; statistics.
### BookWyrm 0.9.3
- Its statuses are `Review`, `Comment` and `Quotation` about a book (`inReplyToBook`) for other BookWyrms, and "pure"
+5
View File
@@ -108,6 +108,11 @@ nodebb.test {
reverse_proxy pasture-nodebb:4567
}
vernissage.test {
tls internal
reverse_proxy pasture-vernissage:8080
}
bookwyrm.test {
tls internal
reverse_proxy pasture-bookwyrm:8000
+25
View File
@@ -0,0 +1,25 @@
# Vernissage 1.43.0: photo sharing in Swift (Vapor), Pixelfed's neighbour: every top-level post carries photos with
# their EXIF (camera, lens, film...), comments thread under them. Its image on SQLite with local storage in its own
# volume and its queues on the shared Redis (database 0, the last free one: without queues it federates nothing), as
# vernissage.test; its TLS client reads the system's roots, over which the pasture's bundle goes. It seeds an
# admin (admin/admin) whose token comes from its login API.
VERNISSAGE_IMAGE=${VERNISSAGE_IMAGE:-docker.io/mczachurski/vernissage-server:v1.43.0}
. "$here/peers/shared.sh"
vernissage_up() {
shared_redis_up
podman volume exists pasture-vernissage || podman volume create --label pasture=1 pasture-vernissage >/dev/null
podman run -d --replace --name pasture-vernissage --label pasture=1 --network $net -v pasture-vernissage:/data \
-e VERNISSAGE_BASEADDRESS=https://vernissage.test -e VERNISSAGE_CONNECTIONSTRING=/data/vernissage.db -e VERNISSAGE_QUEUEURL=redis://redis:6379/0 \
-v "$ca/bundle.pem:/etc/ssl/certs/ca-certificates.crt:z,ro" $VERNISSAGE_IMAGE >/dev/null
for _ in $(seq 1 60); do
site vernissage.test -s -o /dev/null -w '%{http_code}' https://vernissage.test:6443/.well-known/nodeinfo 2>/dev/null | grep -q 200 && break
sleep 2
done
mkdir -p "$here/.state/vernissage"
site vernissage.test -s -X POST https://vernissage.test:6443/api/v1/account/login -H 'Content-Type: application/json' \
-d '{"userNameOrEmail":"admin","password":"admin"}' \
| python3 -c 'import json, sys; print(json.load(sys.stdin).get("accessToken", ""))' > "$here/.state/vernissage/admin.token"
echo "vernissage: https://vernissage.test:6443"
}
+81
View File
@@ -0,0 +1,81 @@
# Vernissage 1.43.0: follows both ways; a photo post with its alt text and camera reaches alice; her like, boost and
# comment land there; her photo post reaches the admin's home and its like and comment come back; a deletion; the
# unfollow; statistics. Driven through its API as its seeded admin.
VE=https://vernissage.test:6443
VT=$(site vernissage.test -s -X POST $VE/api/v1/account/login -H 'Content-Type: application/json' -d '{"userNameOrEmail":"admin","password":"admin"}' | j "print(d['accessToken'])")
ve() { site vernissage.test -s -H "Authorization: Bearer $VT" "$@"; }
ve_json() { local method=$1 path=$2 body=$3; ve -X "$method" "$VE$path" -H 'Content-Type: application/json' -d "$body"; }
# ve_post <json>: a status, waited for when Vernissage says statuses come too often (one every 40 seconds or so)
ve_post() {
local answer
for _ in $(seq 1 12); do
answer=$(ve_json POST /api/v1/statuses "$1")
echo "$answer" | grep -q statusCreationTooFrequent || break
sleep 5
done
echo "$answer"
}
p_home_has() { curl -s -H "$PH" "$P/api/v1/timelines/home?limit=40" | j "print(next(((s.get('reblog') or s)['id'] for s in d if '$1' in ((s.get('reblog') or s)['content'] or '')), ''))"; }
p_status() { curl -s -H "$PH" "$P/api/v1/statuses/$1" | j "print(d.get('$2'))"; }
echo "vernissage"
[ -n "$VT" ] && ok "Vernissage token for admin" || { ko "Vernissage token"; return 1; }
PT=$(privapub_token alice_vern)
PH="Authorization: Bearer $PT"
[ -n "$PT" ] && ok "PrivaPub token for alice_vern" || { ko "PrivaPub token for alice_vern"; return 1; }
run=$(date +%s)
echo " follows"
ve "$VE/api/v1/search?query=alice_vern%40privapub.test&type=users" >/dev/null
ve -o /dev/null -X POST "$VE/api/v1/users/alice_vern@privapub.test/follow"
until_true 45 '[ "$(curl -s -H "$PH" "$P/api/v1/accounts/verify_credentials" | j "print(d[\"followers_count\"])")" -ge 1 ]' \
&& ok "the admin follows alice" || ko "Vernissage's follow never reached alice"
ve_on_p=$(curl -s -H "$PH" "$P/api/v2/search?q=admin@vernissage.test&resolve=true&type=accounts" | j "print(d['accounts'][0]['id'])")
[ -n "$ve_on_p" ] && ok "PrivaPub resolves Vernissage's admin" || ko "PrivaPub cannot resolve Vernissage's admin"
curl -s -o /dev/null -X POST -H "$PH" "$P/api/v1/accounts/$ve_on_p/follow"
until_true 45 '[ "$(curl -s -H "$PH" "$P/api/v1/accounts/relationships?id[]=$ve_on_p" | j "print(d[0][\"following\"])")" = "True" ]' \
&& ok "alice follows the admin (Accept arrived)" || ko "Vernissage's Accept never arrived"
echo " photos"
png="$here/.state/vernissage/photo.png"
make_png "$png"
# (its upload answers with the attachment, which goes back whole with the alt text and the camera)
uploaded=$(ve -X POST "$VE/api/v1/attachments" -F "file=@$png;type=image/png")
att=$(echo "$uploaded" | j "print(d['id'])")
ve_json PUT "/api/v1/attachments/$att" "$(echo "$uploaded" | j "d.update(description='a red square $run', make='Leica', model='M6'); print(json.dumps(d))")" >/dev/null
post=$(ve_post "{\"note\":\"a Vernissage photo $run\",\"visibility\":\"public\",\"sensitive\":false,\"commentsDisabled\":false,\"attachmentIds\":[\"$att\"]}")
post_id=$(echo "$post" | j "print(d['id'])")
until_true 60 '[ -n "$(p_home_has "a Vernissage photo $run")" ]' && ok "the admin's photo reaches alice's home" || ko "the photo never reached alice ($(echo "$post" | head -c 200))"
photo_on_p=$(p_home_has "a Vernissage photo $run")
[ "$(curl -s -H "$PH" "$P/api/v1/statuses/$photo_on_p" | j "print(d['media_attachments'][0]['description'] if d['media_attachments'] else None)")" = "a red square $run" ] \
&& ok "with its picture and alt text" || ko "the photo arrived without its picture or alt text"
curl -s -o /dev/null -X POST -H "$PH" "$P/api/v1/statuses/$photo_on_p/favourite"
until_true 45 '[ "$(ve "$VE/api/v1/statuses/$post_id" | j "print(d[\"favouritesCount\"])")" = "1" ]' && ok "alice's like counts on Vernissage" || ko "alice's like never counted on Vernissage"
curl -s -o /dev/null -X POST -H "$PH" "$P/api/v1/statuses/$photo_on_p/reblog"
until_true 45 '[ "$(ve "$VE/api/v1/statuses/$post_id" | j "print(d[\"reblogsCount\"])")" = "1" ]' && ok "alice's boost counts on Vernissage" || ko "alice's boost never counted on Vernissage"
curl -s -o /dev/null -X POST -H "$PH" $P/api/v1/statuses -d "status=@admin@vernissage.test a PrivaPub comment $run&in_reply_to_id=$photo_on_p&visibility=public"
until_true 45 've "$VE/api/v1/statuses/$post_id/context" | grep -q "a PrivaPub comment $run"' && ok "alice's comment threads under the photo" || ko "alice's comment never reached Vernissage"
echo " alice's photo"
upload=$(curl -s -X POST -H "$PH" "$P/api/v2/media" -F "file=@$png;type=image/png" -F "description=a PrivaPub picture $run")
p_post=$(curl -s -X POST -H "$PH" $P/api/v1/statuses -d "status=a PrivaPub photo for Vernissage $run&visibility=public&media_ids[]=$(echo "$upload" | j "print(d['id'])")")
p_post_id=$(echo "$p_post" | j "print(d['id'])")
until_true 60 've "$VE/api/v1/timelines/home?limit=40" | grep -q "a PrivaPub photo for Vernissage $run"' && ok "alice's photo reaches the admin's home" || ko "alice's photo never reached Vernissage"
p_on_ve=$(ve "$VE/api/v1/timelines/home?limit=40" | j "print(next((s['id'] for s in d.get('data', d) if 'a PrivaPub photo for Vernissage $run' in (s.get('note') or '')), ''))")
ve -o /dev/null -X POST "$VE/api/v1/statuses/$p_on_ve/favourite"
until_true 45 '[ "$(p_status $p_post_id favourites_count)" = "1" ]' && ok "the admin's like counts on PrivaPub" || ko "Vernissage's like never counted"
ve_post "{\"note\":\"a Vernissage comment $run\",\"visibility\":\"public\",\"sensitive\":false,\"commentsDisabled\":false,\"replyToStatusId\":\"$p_on_ve\",\"attachmentIds\":[]}" >/dev/null
until_true 45 '[ "$(curl -s -H "$PH" "$P/api/v1/statuses/$p_post_id/context" | j "print(any(\"a Vernissage comment $run\" in s[\"content\"] for s in d[\"descendants\"]))")" = "True" ]' \
&& ok "the admin's comment threads under alice's photo" || ko "Vernissage's comment missing on PrivaPub"
echo " deletions"
ve -o /dev/null -X DELETE "$VE/api/v1/statuses/$post_id"
until_true 45 '[ "$(curl -s -o /dev/null -w "%{http_code}" -H "$PH" "$P/api/v1/statuses/$photo_on_p")" = "404" ]' \
&& ok "the admin's deletion reaches PrivaPub" || ko "the deleted photo still shows on PrivaPub"
echo " unfollow"
curl -s -o /dev/null -X POST -H "$PH" "$P/api/v1/accounts/$ve_on_p/unfollow"
until_true 45 '[ "$(ve "$VE/api/v1/users/admin" | j "print(d[\"followersCount\"])")" = "0" ]' && ok "alice's unfollow reaches Vernissage" || ko "Vernissage still counts alice"
echo " statistics"
stats_check vernissage.test vernissage