Ghost joins the pasture with its ActivityPub service (Fedify) on the shared MySQL, routed by Caddy as Ghost's own proxy does. scenarios/ghost.sh: follows both ways, the publication's titled Articles with their edit and deletion, likes and boosts both ways, Ghost's reply and note, alice's post in its Network feed, both unfollows and statistics: 22 checks, with no change to PrivaPub. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LsXgEaXee4GCU1hwYgPJXw
77 lines
5.1 KiB
Bash
77 lines
5.1 KiB
Bash
# 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 <method> <path> [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 <method> <path> [json]: the ActivityPub service's own API (/.ghost/activitypub/<path>), 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"}
|
|
}
|