Files
SocialPub/tools/pasture/peers/ghost.sh
T
thepraandClaude Opus 5.5 12b51a211a Pasture: Ghost 6.67 and its ActivityPub service
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
2026-10-06 00:49:18 +02:00

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"}
}