# Castopod 1.15.5: podcasts as ActivityPub actors, their episodes as posts with the audio. The official image (FrankenPHP # on 8080) on the shared MySQL (database castopod), its cache in files (the shared Redis has no database left), HTTPS # left to Caddy (CP_DISABLE_HTTPS). It trusts Caddy's CA through the bundle mounted as its system store. Its database # and superadmin come from its spark commands; its fediverse queue goes out only through `spark fediverse:broadcast`, # run each minute in the container. A podcast is made through its admin pages (they alone take one); episodes through # its REST API, switched on with basic authentication. CASTOPOD_IMAGE=${CASTOPOD_IMAGE:-docker.io/castopod/castopod:1.15.5} CASTOPOD_PASSWORD=Castopod-Pasture-1 CASTOPOD_API=pasture:Castopod-Api-1 . "$here/peers/shared.sh" castopod_spark() { podman exec -w /var/www/html pasture-castopod php spark "$@"; } castopod_up() { shared_mysql_up mysql_db castopod mkdir -p "$here/.state/castopod" [ -s "$here/.state/castopod/env" ] || { echo "CP_BASEURL=https://castopod.test" echo "CP_MEDIA_BASEURL=https://castopod.test" echo -e "CP_DATABASE_HOSTNAME=mysql\nCP_DATABASE_NAME=castopod\nCP_DATABASE_USERNAME=pasture\nCP_DATABASE_PASSWORD=pasture" echo "CP_ANALYTICS_SALT=$(openssl rand -hex 32)" echo -e "CP_CACHE_HANDLER=file\nCP_DISABLE_HTTPS=1" } > "$here/.state/castopod/env" podman volume exists pasture-castopod-media || podman volume create --label pasture=1 pasture-castopod-media >/dev/null podman run -d --replace --name pasture-castopod --network $net --label pasture=1 --env-file "$here/.state/castopod/env" \ -v pasture-castopod-media:/var/www/html/public/media -v "$ca/bundle.pem:/etc/ssl/certs/ca-certificates.crt:z,ro" \ "$CASTOPOD_IMAGE" >/dev/null for _ in $(seq 1 90); do podman exec pasture-castopod test -f /var/www/html/.env 2>/dev/null && castopod_spark list >/dev/null 2>&1 && break sleep 2 done castopod_spark install:init-database >/dev/null 2>&1 # (a second init stops at the first migration it already ran, before seeding: the categories and languages it needs) [ "$(podman exec pasture-mysql mysql -upasture -ppasture castopod -Nse "select count(*) from cp_categories" 2>/dev/null)" != "0" ] \ || castopod_spark db:seed AppSeeder >/dev/null 2>&1 printf '%s\n%s\n' "$CASTOPOD_PASSWORD" "$CASTOPOD_PASSWORD" \ | podman exec -i -w /var/www/html pasture-castopod php spark install:create-superadmin -n admin -e admin@castopod.test >/dev/null 2>&1 || true # its REST API, which makes and publishes episodes, with basic authentication # (the image writes .env anew at each start, read-only) podman exec -u root pasture-castopod sh -c "chmod u+w /var/www/html/.env; grep -q '^restapi.enabled' /var/www/html/.env \ || printf 'restapi.enabled=true\nrestapi.basicAuth=true\nrestapi.basicAuthUsername=${CASTOPOD_API%%:*}\nrestapi.basicAuthPassword=${CASTOPOD_API#*:}\n' >> /var/www/html/.env; chmod a-w /var/www/html/.env" podman exec -d -w /var/www/html pasture-castopod sh -c 'while true; do php spark fediverse:broadcast >/dev/null 2>&1; sleep 30; done' for _ in $(seq 1 60); do site castopod.test -s -o /dev/null -w '%{http_code}' https://castopod.test:6443/.well-known/nodeinfo 2>/dev/null | grep -q 200 && break sleep 2 done castopod_podcast echo "castopod: https://castopod.test:6443" } # cp_web : Castopod's admin pages as its superadmin, signed in (a CSRF token on every form) cp_web() { local jar="$here/.state/castopod/admin.cookies" if ! site castopod.test -s -b "$jar" -o /dev/null -w '%{http_code}' https://castopod.test:6443/cp-admin | grep -q 200; then rm -f "$jar" local token token=$(site castopod.test -s -c "$jar" https://castopod.test:6443/cp-auth/login | grep -oE 'name="csrf_test_name" value="[^"]+"' | head -1 | sed 's/.*value="//;s/"//') site castopod.test -s -o /dev/null -b "$jar" -c "$jar" -X POST https://castopod.test:6443/cp-auth/login --data-urlencode "csrf_test_name=$token" \ -d email=admin@castopod.test --data-urlencode "password=$CASTOPOD_PASSWORD" fi site castopod.test -s -b "$jar" -c "$jar" "$@" } cp_token() { cp_web "https://castopod.test:6443$1" | grep -oE 'name="csrf_test_name" value="[^"]+"' | head -1 | sed 's/.*value="//;s/"//'; } # square(file, side) / banner: the pictures a podcast must have (a 1400px square cover, a 3:1 banner) cp_picture() { python3 -c " import struct,zlib w,h=$2,$3 raw=(b'\x00'+bytes([40,120,200])*w)*h png=b'\x89PNG\r\n\x1a\n'+b''.join(struct.pack('>I',len(c))+t+c+struct.pack('>I',zlib.crc32(t+c)&0xffffffff) for t,c in [(b'IHDR',struct.pack('>IIBBBBB',w,h,8,2,0,0,0)),(b'IDAT',zlib.compress(raw,9)),(b'IEND',b'')]) open('$1','wb').write(png)"; } # the podcast @pod: only its admin pages make one (its banner is required too, and an absent location fails it) castopod_podcast() { [ -n "$(podman exec pasture-mysql mysql -upasture -ppasture castopod -Nse "select id from cp_podcasts where handle = 'pod'" 2>/dev/null)" ] && return 0 local category category=$(podman exec pasture-mysql mysql -upasture -ppasture castopod -Nse "select id from cp_categories where code = 'technology'" 2>/dev/null) cp_picture "$here/.state/castopod/cover.png" 1400 1400 cp_picture "$here/.state/castopod/banner.png" 1500 500 cp_web -o /dev/null -X POST https://castopod.test:6443/cp-admin/podcasts/new -F "csrf_test_name=$(cp_token /cp-admin/podcasts/new)" \ -F "cover=@$here/.state/castopod/cover.png;type=image/png" -F "banner=@$here/.state/castopod/banner.png;type=image/png" \ -F "title=Pasture Podcast" -F "description=A podcast of the pasture" -F type=episodic -F medium=podcast -F language=en \ -F "category=$category" -F parental_advisory=clean -F owner_name=Pasture -F owner_email=admin@castopod.test -F handle=pod \ -F location_name= -F custom_rss= -F verify_txt= castopod_publish_podcast } # a podcast made is a draft: its pages, its episodes' pages and their objects answer 404 until it is published castopod_publish_podcast() { [ "$(podman exec pasture-mysql mysql -upasture -ppasture castopod -Nse "select published_at is not null from cp_podcasts where handle = 'pod'" 2>/dev/null)" = "1" ] && return 0 local token token=$(cp_web https://castopod.test:6443/cp-admin/podcasts/1/publish | grep -oE 'name="csrf_test_name" value="[^"]+"' | tail -1 | sed 's/.*value="//;s/"//') cp_web -o /dev/null -X POST https://castopod.test:6443/cp-admin/podcasts/1/publish --data-urlencode "csrf_test_name=$token" \ -d client_timezone=UTC -d publication_method=now --data-urlencode "message=The pasture's podcast is on the air" }