Pasture: Pixelfed and WordPress join
Pixelfed 0.14.4 (FrankenPHP with Horizon and the scheduler, on the shared Postgres and Redis) and WordPress 6 with ActivityPub 9.3.1 (on a new shared MySQL, WP-Cron run by a sidecar) are peers now, each with its scenario: Pixelfed 26 checks (photos both ways with alt text, its place arriving as Rome with its coordinates, comments, likes, a boost, edits and deletes), WordPress 17 (an Article with its title, our reply, like and boost kept as comments of their kinds, its edit and removal). Pixelfed has a town driver and a pair spec: 145 checks pass, 2 expected (G-0007). What they showed: Pixelfed names our posts by their page (fixed in26dac40); on PostgreSQL its migration making caption nullable never runs, so every remote boost failed (the pasture applies it); it files a DM it fetches as followers-only (G-0007, upstream); Passport refuses a token whose user id equals its client's id. WordPress signs with RFC 9421 first, which PrivaPub now verifies (c5a69d2). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LsXgEaXee4GCU1hwYgPJXw
This commit is contained in:
1 parent
c5a69d240a
commit
01e5aa4ddd
14 files changed
+601
-4
No files matched your search
@@ -0,0 +1,126 @@
|
||||
# Pixelfed 0.14: photo posts (Notes with Image attachments, alt text, a `location` Place), comments, likes, follows and
|
||||
# collections, and FEP-8fcf followers sync. Laravel on FrankenPHP (serversideup's image), on the shared Postgres
|
||||
# (database pixelfed) and Redis (dbs 5 and 6), with Horizon (which runs every federation job) and the scheduler as
|
||||
# sidecars of the same image sharing its storage volume. It trusts Caddy's CA through the bundle mounted as its system
|
||||
# store; its URL guard refuses private addresses with no switch, which the pasture's public-looking subnet passes. Its
|
||||
# admin is pfuser; tokens are Passport personal access tokens made through tinker.
|
||||
PIXELFED_IMAGE=${PIXELFED_IMAGE:-ghcr.io/pixelfed/pixelfed:v0.14.4}
|
||||
PIXELFED_PASSWORD=Pixelfed-Pasture-1
|
||||
. "$here/peers/shared.sh"
|
||||
|
||||
pixelfed_env() {
|
||||
local key_file="$here/.state/pixelfed/app.key"
|
||||
[ -s "$key_file" ] || { mkdir -p "$here/.state/pixelfed"; echo "base64:$(head -c 32 /dev/urandom | base64)" > "$key_file"; }
|
||||
cat <<EOF
|
||||
APP_NAME=Pasture Pixelfed
|
||||
APP_ENV=production
|
||||
APP_KEY=$(cat "$key_file")
|
||||
APP_DEBUG=false
|
||||
APP_URL=https://pixelfed.test
|
||||
APP_DOMAIN=pixelfed.test
|
||||
ADMIN_DOMAIN=pixelfed.test
|
||||
SESSION_DOMAIN=pixelfed.test
|
||||
TRUST_PROXIES=*
|
||||
LOG_CHANNEL=stderr
|
||||
DB_CONNECTION=pgsql
|
||||
DB_HOST=postgres
|
||||
DB_PORT=5432
|
||||
DB_DATABASE=pixelfed
|
||||
DB_USERNAME=pasture
|
||||
DB_PASSWORD=pasture
|
||||
REDIS_CLIENT=phpredis
|
||||
REDIS_HOST=redis
|
||||
REDIS_PORT=6379
|
||||
REDIS_DB=5
|
||||
REDIS_CACHE_DB=6
|
||||
CACHE_DRIVER=redis
|
||||
CACHE_STORE=redis
|
||||
QUEUE_DRIVER=redis
|
||||
QUEUE_CONNECTION=redis
|
||||
SESSION_DRIVER=redis
|
||||
BROADCAST_DRIVER=log
|
||||
MAIL_MAILER=log
|
||||
MAIL_DRIVER=log
|
||||
ACTIVITY_PUB=true
|
||||
AP_REMOTE_FOLLOW=true
|
||||
AP_INBOX=true
|
||||
AP_OUTBOX=true
|
||||
AP_SHAREDINBOX=true
|
||||
OPEN_REGISTRATION=true
|
||||
ENFORCE_EMAIL_VERIFICATION=false
|
||||
OAUTH_ENABLED=true
|
||||
PF_MAX_USERS=100000
|
||||
PF_ENFORCE_MAX_USERS=false
|
||||
INSTANCE_DISCOVER_PUBLIC=true
|
||||
PF_OPTIMIZE_IMAGES=false
|
||||
PF_OPTIMIZE_VIDEOS=false
|
||||
PF_MIN_REGISTRATION_AGE=0
|
||||
EOF
|
||||
}
|
||||
|
||||
pixelfed_run() { # name, then the command (none: the web server)
|
||||
local name=$1; shift
|
||||
podman run -d --replace --name "$name" --network $net --env-file "$here/.state/pixelfed/env" "$@"
|
||||
}
|
||||
|
||||
pixelfed_up() {
|
||||
shared_postgres_up
|
||||
shared_redis_up
|
||||
pg_db pixelfed
|
||||
mkdir -p "$here/.state/pixelfed"
|
||||
pixelfed_env > "$here/.state/pixelfed/env"
|
||||
podman volume exists pasture-pixelfed-storage || podman volume create --label pasture=1 pasture-pixelfed-storage >/dev/null
|
||||
local mounts=(-v pasture-pixelfed-storage:/var/www/html/storage -v "$ca/bundle.pem:/etc/ssl/certs/ca-certificates.crt:z,ro")
|
||||
pixelfed_run pasture-pixelfed -e AUTORUN_ENABLED=true "${mounts[@]}" "$PIXELFED_IMAGE" >/dev/null
|
||||
for _ in $(seq 1 100); do
|
||||
site pixelfed.test -s -o /dev/null -w '%{http_code}' https://pixelfed.test:6443/api/nodeinfo/2.0.json 2>/dev/null | grep -q 200 && break
|
||||
sleep 3
|
||||
done
|
||||
pixelfed_run pasture-pixelfed-horizon "${mounts[@]}" "$PIXELFED_IMAGE" php artisan horizon >/dev/null
|
||||
pixelfed_run pasture-pixelfed-cron "${mounts[@]}" "$PIXELFED_IMAGE" php artisan schedule:work >/dev/null
|
||||
pixelfed_settle
|
||||
echo "pixelfed: https://pixelfed.test:6443"
|
||||
}
|
||||
|
||||
pixelfed_artisan() { podman exec pasture-pixelfed php artisan "$@"; }
|
||||
|
||||
# the instance actor, Passport's keys and personal client, the admin pfuser and its token. Passport takes a token whose
|
||||
# user id equals its client's id for a client-credentials token and refuses it, so the clients are numbered from a
|
||||
# million, past any user. FrankenPHP keeps the keys it read at boot: the web server restarts once they exist.
|
||||
pixelfed_settle() {
|
||||
# 0.14.4's migration making caption and rendered nullable on PostgreSQL checks for a connection named "postgres",
|
||||
# never Laravel's "pgsql", so it does nothing and every remote boost (and DM) fails on NOT NULL: done here instead
|
||||
podman exec pasture-postgres psql -U pasture -d pixelfed -qc \
|
||||
"alter table statuses alter column caption drop not null, alter column rendered drop not null" >/dev/null
|
||||
pixelfed_artisan instance:actor >/dev/null 2>&1 || true
|
||||
if ! podman exec pasture-pixelfed test -s storage/oauth-private.key; then
|
||||
pixelfed_artisan passport:keys --force >/dev/null 2>&1 || true
|
||||
podman restart pasture-pixelfed >/dev/null
|
||||
for _ in $(seq 1 60); do podman exec pasture-pixelfed curl -sf -o /dev/null http://localhost:8080/api/nodeinfo/2.0.json && break; sleep 2; done
|
||||
fi
|
||||
if [ "$(podman exec pasture-postgres psql -U pasture -d pixelfed -Atc "select count(*) from oauth_clients where personal_access_client and not revoked and id >= 1000000")" = "0" ]; then
|
||||
podman exec pasture-postgres psql -U pasture -d pixelfed -qc "update oauth_clients set revoked = true where personal_access_client;
|
||||
select setval('oauth_clients_id_seq', greatest((select coalesce(max(id), 0) from oauth_clients), 1000000));" >/dev/null
|
||||
pixelfed_artisan passport:client --personal --name=pasture --no-interaction >/dev/null 2>&1 || true
|
||||
fi
|
||||
pixelfed_user pfuser admin
|
||||
pixelfed_token pfuser > "$here/.state/pixelfed.token"
|
||||
# the cities a post's place is chosen from (a couple of minutes, once)
|
||||
[ "$(podman exec pasture-postgres psql -U pasture -d pixelfed -Atc "select count(*) from places")" != "0" ] \
|
||||
|| pixelfed_artisan import:cities --no-interaction >/dev/null 2>&1 || true
|
||||
}
|
||||
|
||||
# pixelfed_user <name> [admin]: an account with a confirmed email and the pasture's password (user:admin only asks, and
|
||||
# answers no without a terminal, so an admin is made in the database)
|
||||
pixelfed_user() {
|
||||
pixelfed_artisan user:create --name="$1" --username="$1" --email="$1@pixelfed.test" --password="$PIXELFED_PASSWORD" \
|
||||
--confirm_email=1 --no-interaction >/dev/null 2>&1 || true
|
||||
[ "${2:-}" = "admin" ] && podman exec pasture-postgres psql -U pasture -d pixelfed -qc "update users set is_admin = true where username = '$1'" >/dev/null
|
||||
return 0
|
||||
}
|
||||
|
||||
# pixelfed_token <name>: a personal access token with every scope the Mastodon API asks for
|
||||
pixelfed_token() {
|
||||
podman exec pasture-pixelfed php artisan tinker --execute \
|
||||
"echo App\\Models\\User::where('username', '$1')->first()->createToken('pasture', ['read', 'write', 'follow', 'push'])->accessToken;" 2>/dev/null | tail -1
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
# WordPress 6 with the ActivityPub plugin 9.3.1: posts as Articles (or Notes when short), comments as replies, each
|
||||
# author an actor and the blog one too, likes and boosts kept as comments of their kind. On a shared MySQL 8.4 (database
|
||||
# wordpress). The plugin federates from WP-Cron, so a sidecar runs the due events every few seconds. WordPress sends its
|
||||
# requests through its own CA bundle (wp-includes/certificates), to which Caddy's root is appended; its URL guard refuses
|
||||
# private addresses with no switch, which the pasture's public-looking subnet passes. The admin is wpuser; the town's
|
||||
# accounts are authors with application passwords for the REST API.
|
||||
WORDPRESS_IMAGE=${WORDPRESS_IMAGE:-docker.io/library/wordpress:6-apache}
|
||||
WORDPRESS_CLI_IMAGE=${WORDPRESS_CLI_IMAGE:-docker.io/library/wordpress:cli}
|
||||
WORDPRESS_ACTIVITYPUB=9.3.1
|
||||
WORDPRESS_PASSWORD=Wordpress-Pasture-1
|
||||
. "$here/peers/shared.sh"
|
||||
|
||||
shared_mysql_up() {
|
||||
podman container exists pasture-mysql && return 0
|
||||
podman run -d --replace --name pasture-mysql --network $net --network-alias mysql \
|
||||
-e MYSQL_ROOT_PASSWORD=pasture -e MYSQL_USER=pasture -e MYSQL_PASSWORD=pasture docker.io/library/mysql:8.4 >/dev/null
|
||||
# its first start runs a temporary server without networking to set itself up: only TCP answers when it is ready
|
||||
for _ in $(seq 1 90); do podman exec pasture-mysql mysqladmin ping -h127.0.0.1 --protocol=tcp -uroot -ppasture --silent >/dev/null 2>&1 && return 0; sleep 2; done
|
||||
echo "mysql did not start" >&2; return 1
|
||||
}
|
||||
|
||||
# mysql_db <name>: a database of the shared MySQL for one peer, the pasture user owning it
|
||||
mysql_db() {
|
||||
podman exec pasture-mysql mysql -uroot -ppasture -e \
|
||||
"create database if not exists \`$1\` character set utf8mb4 collate utf8mb4_unicode_ci; grant all on \`$1\`.* to 'pasture'@'%';" 2>/dev/null
|
||||
}
|
||||
|
||||
# wp <args>: wp-cli against the site, sharing its files
|
||||
wp() {
|
||||
podman run --rm --network $net --volumes-from pasture-wordpress --user 33:33 -e HOME=/tmp \
|
||||
-e WORDPRESS_DB_HOST=mysql -e WORDPRESS_DB_USER=pasture -e WORDPRESS_DB_PASSWORD=pasture -e WORDPRESS_DB_NAME=wordpress \
|
||||
"$WORDPRESS_CLI_IMAGE" wp "$@"
|
||||
}
|
||||
|
||||
wordpress_up() {
|
||||
shared_mysql_up
|
||||
mysql_db wordpress
|
||||
podman volume exists pasture-wordpress-html || podman volume create --label pasture=1 pasture-wordpress-html >/dev/null
|
||||
# behind Caddy: https as the proxy says, and the site's own address
|
||||
local extra="if (isset(\$_SERVER['HTTP_X_FORWARDED_PROTO']) && \$_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https') { \$_SERVER['HTTPS'] = 'on'; }
|
||||
define('WP_HOME', 'https://wordpress.test'); define('WP_SITEURL', 'https://wordpress.test'); define('DISABLE_WP_CRON', true);
|
||||
define('FS_METHOD', 'direct');"
|
||||
podman run -d --replace --name pasture-wordpress --network $net \
|
||||
-e WORDPRESS_DB_HOST=mysql -e WORDPRESS_DB_USER=pasture -e WORDPRESS_DB_PASSWORD=pasture -e WORDPRESS_DB_NAME=wordpress \
|
||||
-e WORDPRESS_CONFIG_EXTRA="$extra" -v pasture-wordpress-html:/var/www/html -v "$ca:/pasture/ca:z,ro" \
|
||||
"$WORDPRESS_IMAGE" >/dev/null
|
||||
for _ in $(seq 1 60); do podman exec pasture-wordpress test -f /var/www/html/wp-config.php 2>/dev/null && break; sleep 2; done
|
||||
if ! wp core is-installed >/dev/null 2>&1; then
|
||||
wp core install --url=https://wordpress.test --title="Pasture WordPress" --admin_user=wpuser \
|
||||
--admin_password="$WORDPRESS_PASSWORD" --admin_email=wpuser@wordpress.test --skip-email >/dev/null
|
||||
fi
|
||||
wp rewrite structure '/%postname%/' --hard >/dev/null
|
||||
wp plugin is-installed activitypub 2>/dev/null || wp plugin install activitypub --version=$WORDPRESS_ACTIVITYPUB >/dev/null
|
||||
wp plugin activate activitypub >/dev/null
|
||||
# both the blog and its authors are actors
|
||||
wp option update activitypub_actor_mode actor_blog >/dev/null
|
||||
podman exec pasture-wordpress sh -c 'grep -q "Caddy Local Authority" wp-includes/certificates/ca-bundle.crt || { echo; cat /pasture/ca/root.crt; } >> wp-includes/certificates/ca-bundle.crt'
|
||||
podman run -d --replace --name pasture-wordpress-cron --network $net --volumes-from pasture-wordpress --user 33:33 -e HOME=/tmp \
|
||||
-e WORDPRESS_DB_HOST=mysql -e WORDPRESS_DB_USER=pasture -e WORDPRESS_DB_PASSWORD=pasture -e WORDPRESS_DB_NAME=wordpress \
|
||||
--entrypoint sh "$WORDPRESS_CLI_IMAGE" -c 'while :; do wp cron event run --due-now >/dev/null 2>&1; sleep 5; done' >/dev/null
|
||||
wait_http https://wordpress.test:6443/ 1 >/dev/null 2>&1 || true
|
||||
for _ in $(seq 1 60); do
|
||||
site wordpress.test -s -o /dev/null -w '%{http_code}' "https://wordpress.test:6443/.well-known/webfinger?resource=acct:wpuser@wordpress.test" 2>/dev/null | grep -q 200 && break
|
||||
sleep 2
|
||||
done
|
||||
wordpress_app_password wpuser > "$here/.state/wordpress.token"
|
||||
echo "wordpress: https://wordpress.test:6443"
|
||||
}
|
||||
|
||||
# wordpress_user <name>: an author (who publishes, and so is an actor)
|
||||
wordpress_user() {
|
||||
wp user get "$1" >/dev/null 2>&1 || wp user create "$1" "$1@wordpress.test" --role=author --user_pass="$WORDPRESS_PASSWORD" >/dev/null
|
||||
}
|
||||
|
||||
# wordpress_app_password <name>: "name:password" for the REST API's basic authentication
|
||||
wordpress_app_password() {
|
||||
echo "$1:$(wp user application-password create "$1" pasture --porcelain)"
|
||||
}
|
||||
Reference in new issue
Block a user