From a58413485800f837670df67f30251753a3cbfdd7 Mon Sep 17 00:00:00 2001 From: thepra Date: Tue, 6 Oct 2026 00:32:46 +0200 Subject: [PATCH] Pasture: Owncast 0.3.0 Owncast joins the pasture with federation turned on through its admin API; scenarios/owncast.sh has alice follow the stream, receive its admin's message, like and boost it (both show in Owncast's admin), and unfollow: 13 checks, with no change to PrivaPub. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01LsXgEaXee4GCU1hwYgPJXw --- CLAUDE.md | 3 +++ FEDERATION.md | 1 + docs/INTEROP.md | 9 +++++++ tools/pasture/Caddyfile | 5 ++++ tools/pasture/peers/owncast.sh | 22 +++++++++++++++++ tools/pasture/scenarios/owncast.sh | 39 ++++++++++++++++++++++++++++++ 6 files changed, 79 insertions(+) create mode 100644 tools/pasture/peers/owncast.sh create mode 100644 tools/pasture/scenarios/owncast.sh diff --git a/CLAUDE.md b/CLAUDE.md index 501f632..76f8f6f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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. +- **Owncast (0.3.0):** its image, its data in the `pasture-owncast` volume, federation turned on through its admin + API (`owncast_admin`, its default admin / abc123) with the server's address and the actor's name. + `scenarios/owncast.sh`, 13 checks. - **WriteFreely (0.17.2):** built from its release tarball with `openssl` beside it (`images/writefreely`, it makes each blog's keys with the command), on SQLite in the `pasture-writefreely` volume, which the scenario reads from the host. wfuser and its blog come from its CLI; its API takes a login. `scenarios/writefreely.sh`, 13 checks. diff --git a/FEDERATION.md b/FEDERATION.md index 148e0b9..98fcdc5 100644 --- a/FEDERATION.md +++ b/FEDERATION.md @@ -36,6 +36,7 @@ and every run starting clean, with signed fetches required (as privapub.thepra.d - **Mitra 5.9.1** - **snac2 2.95** - **WriteFreely 0.17.2** +- **Owncast 0.3.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** diff --git a/docs/INTEROP.md b/docs/INTEROP.md index 5cc6851..b2cc908 100644 --- a/docs/INTEROP.md +++ b/docs/INTEROP.md @@ -753,6 +753,15 @@ 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. +### Owncast 0.3.0 + +- The instance is one `Service` actor (`/federation/user/`): it is followed, never follows, posts when it goes + live and what its admin sends, and lists the likes, boosts and follows it receives as "fediverse engagement". + Federation is off until the admin turns it on with the server's address. +- **Pasture evidence (2026-10-05, `tools/pasture/scenarios/owncast.sh`):** 13 checks pass, with no change to PrivaPub: + alice follows the stream and is listed, the admin's message reaches her home, her like and boost show in its admin, + the unfollow, statistics. + ### WriteFreely 0.17.2 - Each blog is an actor (`/api/collections/`) whose posts go out as `Article`s with their title and the whole diff --git a/tools/pasture/Caddyfile b/tools/pasture/Caddyfile index 55fb04d..6fada18 100644 --- a/tools/pasture/Caddyfile +++ b/tools/pasture/Caddyfile @@ -108,6 +108,11 @@ nodebb.test { reverse_proxy pasture-nodebb:4567 } +owncast.test { + tls internal + reverse_proxy pasture-owncast:8080 +} + writefreely.test { tls internal reverse_proxy pasture-writefreely:8080 diff --git a/tools/pasture/peers/owncast.sh b/tools/pasture/peers/owncast.sh new file mode 100644 index 0000000..ead0098 --- /dev/null +++ b/tools/pasture/peers/owncast.sh @@ -0,0 +1,22 @@ +# Owncast 0.3.0: a live video server whose instance is one ActivityPub actor (a Service): people follow it, it tells them +# when it goes live and posts what its admin sends, and their likes, boosts and follows show in its admin. As +# owncast.test, its data in its own volume; Go trusts the bundle the pasture mounts over the system one. Federation is +# turned on through its admin API (admin / abc123, its default), with the server's address and the actor's name. +OWNCAST_IMAGE=${OWNCAST_IMAGE:-docker.io/owncast/owncast:0.3.0} + +owncast_up() { + podman volume exists pasture-owncast || podman volume create --label pasture=1 pasture-owncast >/dev/null + podman run -d --replace --name pasture-owncast --label pasture=1 --network $net -v pasture-owncast:/app/data \ + -v "$ca/bundle.pem:/etc/ssl/certs/ca-certificates.crt:z,ro" $OWNCAST_IMAGE >/dev/null + for _ in $(seq 1 60); do + site owncast.test -s -o /dev/null -w '%{http_code}' https://owncast.test:6443/api/status 2>/dev/null | grep -q 200 && break + sleep 1 + done + owncast_admin config/serverurl '"https://owncast.test"' + owncast_admin config/federation/username '"owncast"' + owncast_admin config/federation/enable 'true' + echo "owncast: https://owncast.test:6443" +} + +# owncast_admin : an admin API change, {"value": ...} +owncast_admin() { site owncast.test -s -o /dev/null -u admin:abc123 -X POST "https://owncast.test:6443/api/admin/$1" -H 'Content-Type: application/json' -d "{\"value\":$2}"; } diff --git a/tools/pasture/scenarios/owncast.sh b/tools/pasture/scenarios/owncast.sh new file mode 100644 index 0000000..2b6786b --- /dev/null +++ b/tools/pasture/scenarios/owncast.sh @@ -0,0 +1,39 @@ +# Owncast 0.3.0: alice follows the stream; a message its admin sends reaches her home; her like and boost show in its +# admin's list of fediverse actions; the unfollow; statistics. Owncast's one actor follows nobody and takes no replies. +# Driven through its admin API. +OC=https://owncast.test:6443 +oc() { site owncast.test -s -u admin:abc123 "$@"; } +p_home_has() { 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'] or '')), ''))"; } +# oc_actions : how many fediverse actions of that type its admin lists from alice +oc_actions() { oc "$OC/api/admin/federation/actions?limit=100" | j "r = d.get('results', d) if isinstance(d, dict) else d; print(sum(1 for a in r if a.get('type') == '$1' and 'alice_owncast' in (a.get('actorIRI') or '')))"; } + +echo "owncast" +[ "$(oc "$OC/api/admin/serverconfig" | j "print(d['federation']['enabled'])")" = "True" ] && ok "Owncast federates" || { ko "Owncast's federation is off"; return 1; } +PT=$(privapub_token alice_owncast) +PH="Authorization: Bearer $PT" +[ -n "$PT" ] && ok "PrivaPub token for alice_owncast" || { ko "PrivaPub token for alice_owncast"; return 1; } +run=$(date +%s) + +echo " following the stream" +oc_on_p=$(curl -s -H "$PH" "$P/api/v2/search?q=owncast@owncast.test&resolve=true&type=accounts" | j "print(d['accounts'][0]['id'])") +[ -n "$oc_on_p" ] && ok "PrivaPub resolves the stream's actor" || ko "PrivaPub cannot resolve the stream's actor" +curl -s -o /dev/null -X POST -H "$PH" "$P/api/v1/accounts/$oc_on_p/follow" +until_true 45 '[ "$(curl -s -H "$PH" "$P/api/v1/accounts/relationships?id[]=$oc_on_p" | j "print(d[0][\"following\"])")" = "True" ]' \ + && ok "alice follows the stream (Accept arrived)" || ko "the stream's Accept never arrived" +until_true 30 'oc "$OC/api/admin/followers" | grep -q alice_owncast' && ok "and is among its followers" || ko "Owncast does not list alice" + +echo " posts" +oc -o /dev/null -X POST "$OC/api/admin/federation/send" -H 'Content-Type: application/json' -d "{\"value\":\"an Owncast message $run\"}" +until_true 45 '[ -n "$(p_home_has "an Owncast message $run")" ]' && ok "the stream's message reaches alice's home" || ko "the stream's message never reached alice" +on_p=$(p_home_has "an Owncast message $run") +curl -s -o /dev/null -X POST -H "$PH" "$P/api/v1/statuses/$on_p/favourite" +until_true 45 '[ "$(oc_actions FEDIVERSE_ENGAGEMENT_LIKE)" -ge 1 ]' && ok "alice's like shows in its admin" || ko "Owncast never saw alice's like ($(oc_actions FEDIVERSE_ENGAGEMENT_LIKE))" +curl -s -o /dev/null -X POST -H "$PH" "$P/api/v1/statuses/$on_p/reblog" +until_true 45 '[ "$(oc_actions FEDIVERSE_ENGAGEMENT_REPOST)" -ge 1 ]' && ok "alice's boost shows in its admin" || ko "Owncast never saw alice's boost" + +echo " unfollow" +curl -s -o /dev/null -X POST -H "$PH" "$P/api/v1/accounts/$oc_on_p/unfollow" +until_true 45 '! oc "$OC/api/admin/followers" | grep -q alice_owncast' && ok "alice's unfollow reaches the stream" || ko "Owncast still lists alice" + +echo " statistics" +stats_check owncast.test owncast