# Ghost 6.67 with its ActivityPub service 1.2.14 (Fedify): a publication whose posts go out as Articles from its # @index actor, and which follows, likes, reposts and replies from its "Network" screen. Ghost and the service share the # pasture's MySQL (databases ghost and activitypub); Caddy sends /.ghost/activitypub, WebFinger and NodeInfo to the # service and everything else to Ghost, as Ghost's own Caddyfile does. Both trust the CA through NODE_EXTRA_CA_CERTS. # The owner is made through Ghost's setup API, staff device verification is off (the pasture sends no mail), and the # service learns of the site the first time Ghost's identity token asks it. GHOST_IMAGE=${GHOST_IMAGE:-docker.io/library/ghost:6.67.0-alpine} GHOST_AP_IMAGE=${GHOST_AP_IMAGE:-ghcr.io/tryghost/activitypub:1.2.14} GHOST_AP_MIGRATIONS_IMAGE=${GHOST_AP_MIGRATIONS_IMAGE:-ghcr.io/tryghost/activitypub-migrations:1.2.14} # (Ghost refuses a password that repeats the site's or the owner's name) GHOST_PASSWORD=Lantern-Meadow-Quartz-71 . "$here/peers/shared.sh" ghost_up() { shared_mysql_up mysql_db ghost mysql_db activitypub podman run --rm --network $net -e MYSQL_DB='mysql://pasture:pasture@tcp(mysql:3306)/activitypub' $GHOST_AP_MIGRATIONS_IMAGE >/dev/null 2>&1 podman volume exists pasture-ghost || podman volume create --label pasture=1 pasture-ghost >/dev/null podman run -d --replace --name pasture-ghost-ap --label pasture=1 --network $net -v pasture-ghost:/opt/activitypub/content \ -e NODE_ENV=production -e MYSQL_HOST=mysql -e MYSQL_USER=pasture -e MYSQL_PASSWORD=pasture -e MYSQL_DATABASE=activitypub \ -e LOCAL_STORAGE_PATH=/opt/activitypub/content/images/activitypub -e LOCAL_STORAGE_HOSTING_URL=https://ghost.test/content/images/activitypub \ -e ALLOW_PRIVATE_ADDRESS=true -e NODE_EXTRA_CA_CERTS=/pasture/ca/root.crt -v "$ca:/pasture/ca:z,ro" $GHOST_AP_IMAGE >/dev/null podman run -d --replace --name pasture-ghost --label pasture=1 --network $net -v pasture-ghost:/var/lib/ghost/content \ -e NODE_ENV=production -e url=https://ghost.test -e database__client=mysql -e database__connection__host=mysql \ -e database__connection__user=pasture -e database__connection__password=pasture -e database__connection__database=ghost \ -e security__staffDeviceVerification=false -e NODE_EXTRA_CA_CERTS=/pasture/ca/root.crt -v "$ca:/pasture/ca:z,ro" \ $GHOST_IMAGE >/dev/null for _ in $(seq 1 120); do site ghost.test -s -o /dev/null -w '%{http_code}' https://ghost.test:6443/ghost/api/admin/site/ 2>/dev/null | grep -q 200 && break sleep 2 done ghost_settle echo "ghost: https://ghost.test:6443" } # ghost_api [json]: Ghost's admin API as the owner, with the session cookie ghost_settle keeps ghost_api() { local method=$1 path=$2 body=${3:-} site ghost.test -s -X "$method" "https://ghost.test:6443/ghost/api/admin/$path" -b "$here/.state/ghost/cookies" -c "$here/.state/ghost/cookies" \ -H 'Origin: https://ghost.test' -H 'Content-Type: application/json' ${body:+-d "$body"} } # the owner (ghostuser@ghost.test), its session, and the ActivityPub service told of the site ghost_settle() { local st="$here/.state/ghost" mkdir -p "$st" site ghost.test -s -o /dev/null -X POST https://ghost.test:6443/ghost/api/admin/authentication/setup/ -H 'Origin: https://ghost.test' \ -H 'Content-Type: application/json' \ -d "{\"setup\":[{\"name\":\"Ghost User\",\"email\":\"ghostuser@ghost.test\",\"password\":\"$GHOST_PASSWORD\",\"blogTitle\":\"Ghost pasture\"}]}" rm -f "$st/cookies" site ghost.test -s -o /dev/null -X POST https://ghost.test:6443/ghost/api/admin/session/ -c "$st/cookies" -H 'Origin: https://ghost.test' \ -H 'Content-Type: application/json' -d "{\"username\":\"ghostuser@ghost.test\",\"password\":\"$GHOST_PASSWORD\"}" ghost_ap GET v1/site >/dev/null # Ghost sets up the webhooks that publish its posts when it starts, and only once it has an owner and the service # knows the site: the first time, it starts again if [ "$(podman exec pasture-mysql mysql -uroot -ppasture -N ghost -e 'select count(*) from webhooks' 2>/dev/null)" != "4" ]; then podman restart pasture-ghost >/dev/null for _ in $(seq 1 60); do site ghost.test -s -o /dev/null -w '%{http_code}' https://ghost.test:6443/ghost/api/admin/site/ 2>/dev/null | grep -q 200 && break sleep 2 done rm -f "$st/cookies" site ghost.test -s -o /dev/null -X POST https://ghost.test:6443/ghost/api/admin/session/ -c "$st/cookies" -H 'Origin: https://ghost.test' \ -H 'Content-Type: application/json' -d "{\"username\":\"ghostuser@ghost.test\",\"password\":\"$GHOST_PASSWORD\"}" fi } # ghost_ap [json]: the ActivityPub service's own API (/.ghost/activitypub/), with Ghost's identity token ghost_ap() { local method=$1 path=$2 body=${3:-} token token=$(ghost_api GET identities/ | python3 -c 'import json, sys; print(json.load(sys.stdin)["identities"][0]["token"])') # (the service names the site after the request's host, so the call says ghost.test without the workstation's port) site ghost.test -s -X "$method" "https://ghost.test:6443/.ghost/activitypub/$path" -H 'Host: ghost.test' -H "Authorization: Bearer $token" \ -H 'Content-Type: application/json' ${body:+-d "$body"} }