From f9eb79553ff10e71e7957c722bd47c7e82ccc543 Mon Sep 17 00:00:00 2001 From: clover caruso Date: Wed, 13 May 2026 01:51:04 -0700 Subject: [PATCH] feat: atproto stuff --- compose.yaml | 147 +++++++++++++++++++++++++ config/Caddyfile | 46 +++++++- config/pds-entry.sh | 24 ++++ config/spindle/Dockerfile | 16 +++ config/spindle/entry.sh | 5 + config/xmpp/Dockerfile | 11 ++ config/xmpp/conf.d/paperclover.cfg.lua | 16 +++ config/xmpp/entry.sh | 60 ++++++++++ generate-env.sh | 30 ++++- 9 files changed, 352 insertions(+), 3 deletions(-) create mode 100644 config/pds-entry.sh create mode 100644 config/spindle/Dockerfile create mode 100644 config/spindle/entry.sh create mode 100644 config/xmpp/Dockerfile create mode 100644 config/xmpp/conf.d/paperclover.cfg.lua create mode 100644 config/xmpp/entry.sh diff --git a/compose.yaml b/compose.yaml index bc85e9366606b5395896636d5515c2f9212b386e..36c8cef04a223b0e9fd0c61411405726eba4fbd2 100755 --- a/compose.yaml +++ b/compose.yaml @@ -215,6 +215,76 @@ services: - "${APP_ROOT}/forgejo-runner:/data" restart: unless-stopped command: forgejo-runner daemon + knot: # port 22 + container_name: knot + image: knot:latest + build: + context: https://tangled.org/tangled.org/3lqs6zdi4nt22.git + dockerfile: Dockerfile + args: + TAG: "${KNOT_IMAGE_TAG:-v1.14.0-alpha}" + UID: "${USER_ID:?}" + GID: "${GROUP_ID:?}" + pull_policy: build + depends_on: + knot-init: + condition: service_completed_successfully + environment: + KNOT_SERVER_HOSTNAME: "knot.${HOME_DOMAIN:?}" + KNOT_SERVER_OWNER: "${KNOT_SERVER_OWNER:?}" + KNOT_SERVER_DB_PATH: /app/knotserver.db + KNOT_REPO_SCAN_PATH: /home/git/repositories + KNOT_SERVER_INTERNAL_LISTEN_ADDR: localhost:5444 + APPVIEW_ENDPOINT: "${KNOT_APPVIEW_ENDPOINT:-https://tangled.org}" + ports: + - "${KNOT_SSH_PORT:-2222}:22" + restart: unless-stopped + volumes: + - "${APP_ROOT}/knot/keys:/etc/ssh/keys" + - "${APP_ROOT}/knot/repositories:/home/git/repositories" + - "${APP_ROOT}/knot/server:/app" + labels: + net.paperclover.list.name: Tangled Knot + net.paperclover.list.domain: knot + net.paperclover.list.priority: 106 + knot-init: + container_name: knot-init + image: alpine:3.22 + user: "0:0" + command: ["sh", "-c", "chown -R ${USER_ID:?}:${GROUP_ID:?} /knot"] + volumes: + - "${APP_ROOT}/knot:/knot" + spindle: # port 6555 + container_name: spindle + build: + context: ./config/spindle + dockerfile: Dockerfile + args: + TAG: "${SPINDLE_IMAGE_TAG:-v1.14.0-alpha}" + pull_policy: build + depends_on: + docker-in-docker: + condition: service_started + environment: + DOCKER_HOST: tcp://docker-in-docker:2375 + SPINDLE_SERVER_HOSTNAME: "spindle.${HOME_DOMAIN:?}" + SPINDLE_SERVER_OWNER: "${SPINDLE_SERVER_OWNER:?}" + SPINDLE_SERVER_DB_PATH: /data/spindle.db + SPINDLE_SERVER_LOG_DIR: /data/logs + SPINDLE_SERVER_TAP_EMBED: "true" + SPINDLE_SERVER_TAP_URL: http://127.0.0.1:2480 + SPINDLE_SERVER_TAP_BIND: 127.0.0.1:2480 + SPINDLE_SERVER_TAP_DB_PATH: /data/tap.db + SPINDLE_NIXERY_PIPELINES_NIXERY: "${SPINDLE_NIXERY_PIPELINES_NIXERY:-nixery.tangled.sh}" + SPINDLE_NIXERY_PIPELINES_WORKFLOW_TIMEOUT: "${SPINDLE_NIXERY_PIPELINES_WORKFLOW_TIMEOUT:-10m}" + user: "$USER_ID:$GROUP_ID" + restart: unless-stopped + volumes: + - "${APP_ROOT}/spindle:/data" + labels: + net.paperclover.list.name: Tangled Spindle + net.paperclover.list.domain: spindle + net.paperclover.list.priority: 107 docker-in-docker: container_name: docker-in-docker image: docker:dind @@ -380,6 +450,43 @@ services: net.paperclover.list.domain: opencode net.paperclover.list.priority: 70 net.paperclover.list.access: personal + # AT Protocol account hosting + pds: # port 3000 + container_name: pds + image: ghcr.io/bluesky-social/pds:0.4 + entrypoint: ["/bin/sh", "/custom/pds-entry.sh"] + user: "0:0" + restart: unless-stopped + environment: + HOME_DOMAIN: "${HOME_DOMAIN:?}" + PDS_HOSTNAME: "at.${HOME_DOMAIN:?}" + PDS_JWT_SECRET: "${PDS_JWT_SECRET:?}" + PDS_ADMIN_PASSWORD: "${PDS_ADMIN_PASSWORD:?}" + PDS_PLC_ROTATION_KEY_K256_PRIVATE_KEY_HEX: "${PDS_PLC_ROTATION_KEY_K256_PRIVATE_KEY_HEX:?}" + PDS_DATA_DIRECTORY: /pds + PDS_BLOBSTORE_DISK_LOCATION: /pds/blocks + PDS_DID_PLC_URL: https://plc.directory + PDS_BSKY_APP_VIEW_URL: https://api.bsky.app + PDS_BSKY_APP_VIEW_DID: did:web:api.bsky.app + PDS_REPORT_SERVICE_URL: https://mod.bsky.app + PDS_REPORT_SERVICE_DID: did:plc:ar7c4by46qjdydhdevvrndac + PDS_CRAWLERS: https://bsky.network + LOG_ENABLED: "true" + PDS_RATE_LIMITS_ENABLED: "${PDS_RATE_LIMITS_ENABLED:-true}" + PDS_INVITE_REQUIRED: "${PDS_INVITE_REQUIRED:-false}" + MAILER_ADDRESS: "${MAILER_ADDRESS}" + MAILER_USERNAME: "${MAILER_USERNAME}" + MAILER_PASSWORD: "${MAILER_PASSWORD}" + PDS_CONTACT_EMAIL_ADDRESS: "${PDS_CONTACT_EMAIL_ADDRESS:?}" + volumes: + - "${APP_ROOT}/pds:/pds" + - "./config/pds-entry.sh:/custom/pds-entry.sh:ro" + healthcheck: + test: ["CMD", "wget", "--quiet", "--spider", "http://127.0.0.1:3000/xrpc/_health"] + interval: 10s + timeout: 5s + retries: 12 + start_period: 20s # databases postgres: container_name: postgres @@ -552,6 +659,46 @@ services: net.paperclover.list.domain: sync net.paperclover.list.priority: 31 net.paperclover.list.access: personal + # chat + xmpp: # ports 5222, 5269 + container_name: xmpp + build: + context: ./config/xmpp + dockerfile: Dockerfile + pull_policy: build + ports: + - 5222:5222 + - 5269:5269 + - 5000:5000 + environment: + USER_ID: "$USER_ID" + GROUP_ID: "$GROUP_ID" + XMPP_HOSTNAME: "xmpp.${HOME_DOMAIN:?}" + XMPP_ADMIN_USER: "${XMPP_ADMIN_USER:?}" + XMPP_ADMIN_PASSWORD: "${XMPP_ADMIN_PASSWORD:?}" + ACME_EMAIL: "${ADMIN_EMAIL:?}" + PROSODY_VIRTUAL_HOSTS: "xmpp.${HOME_DOMAIN:?}" + PROSODY_NETWORK_HOSTNAME: "xmpp.${HOME_DOMAIN:?}" + PROSODY_ADMINS: "${XMPP_ADMIN_USER:?}@xmpp.${HOME_DOMAIN:?}" + PROSODY_COMPONENTS: "conference.xmpp.${HOME_DOMAIN:?}:muc,upload.xmpp.${HOME_DOMAIN:?}:http_file_share,proxy.xmpp.${HOME_DOMAIN:?}:proxy65" + PROSODY_MUC_MODULES: "muc_mam,muc_unique" + PROSODY_UPLOAD_LIMIT_MB: "100" + PROSODY_UPLOAD_STORAGE_GB: "10" + PROSODY_RETENTION_DAYS: "7" + PROSODY_LOGLEVEL: "info" + restart: unless-stopped + volumes: + - "${APP_ROOT}/xmpp/data:/var/lib/prosody" + - "${APP_ROOT}/xmpp/certs:/etc/prosody/certs" + - "${APP_ROOT}/xmpp/letsencrypt:/etc/letsencrypt" + - "./config/xmpp/conf.d:/etc/prosody/conf.d:ro" + labels: + net.paperclover.list.name: Paper Clover XMPP + net.paperclover.list.domain: xmpp + net.paperclover.list.priority: 108 + depends_on: + caddy: + condition: service_started # administration openspeedtest: # port 3000 restart: unless-stopped diff --git a/config/Caddyfile b/config/Caddyfile index 261244bd4bde1732e6bea884837018e927c7a2f3..1f05b8f3154a87118505e153e3357d2ab18308d6 100644 --- a/config/Caddyfile +++ b/config/Caddyfile @@ -114,6 +114,15 @@ jkt.{$HOME_DOMAIN} { header_up Authorization "Basic dXNlcm5hbWU6cGFzc3dvcmQ=" } } +at.{$HOME_DOMAIN} { + reverse_proxy "http://pds:3000" +} +knot.{$HOME_DOMAIN} { + reverse_proxy "http://knot:5555" +} +spindle.{$HOME_DOMAIN} { + reverse_proxy "http://spindle:6555" +} music.{$HOME_DOMAIN} { handle_path /app/sw.js { header Content-Type "application/javascript" @@ -189,6 +198,41 @@ speedtest.{$HOME_DOMAIN} { sync.{$HOME_DOMAIN} { import reverse_proxy_auth "http://syncthing:8384" admin } +xmpp.{$HOME_DOMAIN} { + tls { + on_demand + } + + handle /share* { + reverse_proxy "https://xmpp:5281" { + transport http { + tls_server_name xmpp.{$HOME_DOMAIN} + tls_insecure_skip_verify + } + } + } + + handle /xmpp-websocket* { + reverse_proxy "https://xmpp:5281" { + transport http { + tls_server_name xmpp.{$HOME_DOMAIN} + tls_insecure_skip_verify + } + } + } + + header Content-Type "text/plain; charset=utf-8" + respond "XMPP service. Use port 5222 for clients and port 5269 for federation." 200 +} + +http://xmpp.{$HOME_DOMAIN} { + handle /.well-known/acme-challenge/* { + reverse_proxy "http://xmpp:80" + } + + header Content-Type "text/plain; charset=utf-8" + respond "XMPP service. Use port 5222 for clients and port 5269 for federation." 200 +} # evil inc temporary infrastructure git.evil.inc { @@ -241,5 +285,3 @@ nas.paperclover.net { local.test, *.local.test { tls internal } - - diff --git a/config/pds-entry.sh b/config/pds-entry.sh new file mode 100644 index 0000000000000000000000000000000000000000..0e1eac739113632353ae0f04724016b971c79def --- /dev/null +++ b/config/pds-entry.sh @@ -0,0 +1,24 @@ +#!/bin/sh +set -eu + +encode_uri_component() { + node -e 'process.stdout.write(encodeURIComponent(process.argv[1]))' "$1" +} + +: "${MAILER_ADDRESS:?Missing MAILER_ADDRESS}" +: "${MAILER_USERNAME:?Missing MAILER_USERNAME}" +: "${MAILER_PASSWORD:?Missing MAILER_PASSWORD}" +: "${HOME_DOMAIN:?Missing HOME_DOMAIN}" + +# PDS expects a single SMTP URL, so encode credentials before composing it. +mailer_username=$(encode_uri_component "$MAILER_USERNAME") +mailer_password=$(encode_uri_component "$MAILER_PASSWORD") +export PDS_EMAIL_SMTP_URL="smtps://${mailer_username}:${mailer_password}@${MAILER_ADDRESS}:465/" +export PDS_EMAIL_FROM_ADDRESS="noreply@${HOME_DOMAIN}" + + +if [ "$#" -eq 0 ]; then + set -- node --enable-source-maps index.js +fi + +exec dumb-init -- "$@" diff --git a/config/spindle/Dockerfile b/config/spindle/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..dcf9b56aa70e73e5e8df84947dc4c45309fbaf33 --- /dev/null +++ b/config/spindle/Dockerfile @@ -0,0 +1,16 @@ +FROM golang:1.25-alpine AS builder +ENV CGO_ENABLED=1 + +ARG TAG="v1.14.0-alpha" + +WORKDIR /app +RUN apk add --no-cache git gcc musl-dev +RUN git clone -b "${TAG}" https://tangled.org/@tangled.org/core . +RUN go build -o /usr/local/bin/spindle -ldflags '-s -w -extldflags "-static"' ./cmd/spindle + +FROM alpine:latest +RUN apk add --no-cache ca-certificates +COPY --from=builder /usr/local/bin/spindle /usr/local/bin/spindle +COPY entry.sh /entry.sh +EXPOSE 6555 +ENTRYPOINT ["/bin/sh", "/entry.sh"] diff --git a/config/spindle/entry.sh b/config/spindle/entry.sh new file mode 100644 index 0000000000000000000000000000000000000000..8c51c0193088bf0bbe5230bac52cf43f0428edad --- /dev/null +++ b/config/spindle/entry.sh @@ -0,0 +1,5 @@ +#!/bin/sh +set -e + +mkdir -p "${SPINDLE_SERVER_LOG_DIR:-/data/logs}" +exec /usr/local/bin/spindle diff --git a/config/xmpp/Dockerfile b/config/xmpp/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..2ce63355fef1542ada21bcc2a341c9d07998ddd8 --- /dev/null +++ b/config/xmpp/Dockerfile @@ -0,0 +1,11 @@ +FROM prosodyim/prosody:13.0 + +RUN apt-get update \ + && apt-get install -y --no-install-recommends certbot \ + && rm -rf /var/lib/apt/lists/* + +COPY entry.sh /entry.sh +RUN chmod 755 /entry.sh + +ENTRYPOINT ["/usr/bin/tini", "--", "/entry.sh"] +CMD ["prosody", "-F"] diff --git a/config/xmpp/conf.d/paperclover.cfg.lua b/config/xmpp/conf.d/paperclover.cfg.lua new file mode 100644 index 0000000000000000000000000000000000000000..4dcf732bf9020146d8fe3c4b142521e15a6af468 --- /dev/null +++ b/config/xmpp/conf.d/paperclover.cfg.lua @@ -0,0 +1,16 @@ +allow_registration = true +c2s_require_encryption = true +http_external_url = "https://" .. Lua.os.getenv("XMPP_HOSTNAME") .. "/" +contact_info = { + abuse = { "mailto:" .. Lua.os.getenv("ACME_EMAIL") }; + admin = { "mailto:" .. Lua.os.getenv("ACME_EMAIL") }; +} + +modules_enabled:append({ + "cloud_notify"; + "smacks"; + "csi"; + "csi_simple"; + "server_contact_info"; + "websocket"; +}) diff --git a/config/xmpp/entry.sh b/config/xmpp/entry.sh new file mode 100644 index 0000000000000000000000000000000000000000..354b8e673c7101612a7c823c2ffb1ac463f5f424 --- /dev/null +++ b/config/xmpp/entry.sh @@ -0,0 +1,60 @@ +#!/bin/bash +set -euo pipefail + +if [[ "${1:-}" != "prosody" ]]; then + exec prosodyctl "$@" +fi + +host_uid="${USER_ID:-}" +host_gid="${GROUP_ID:-}" + +if [[ -n "$host_gid" ]]; then + groupmod -o -g "$host_gid" prosody +fi + +if [[ -n "$host_uid" ]]; then + usermod -o -u "$host_uid" prosody +fi + +mkdir -p /etc/prosody/certs /var/lib/prosody /var/run/prosody +chown -R prosody:prosody /etc/prosody/certs /var/lib/prosody /var/run/prosody + +install_acme_cert() { + local host="$1" + local live_dir="/etc/letsencrypt/live/$host" + + if [[ -f "$live_dir/fullchain.pem" ]]; then + certbot renew --non-interactive --quiet || true + else + certbot certonly \ + --standalone \ + --http-01-port 80 \ + --preferred-challenges http \ + --non-interactive \ + --agree-tos \ + --email "${ACME_EMAIL:?}" \ + -d "$host" + fi + + cp "$live_dir/fullchain.pem" "/etc/prosody/certs/$host.crt" + cp "$live_dir/privkey.pem" "/etc/prosody/certs/$host.key" + chown prosody:prosody "/etc/prosody/certs/$host.crt" "/etc/prosody/certs/$host.key" + chmod 640 "/etc/prosody/certs/$host.key" +} + +register_account() { + local username="$1" + local password="$2" + local host="$3" + + if [[ -z "$username" || -z "$password" ]]; then + return 0 + fi + + runuser -u prosody -- prosodyctl register "$username" "$host" "$password" >/dev/null 2>&1 || true +} + +install_acme_cert "${XMPP_HOSTNAME:?}" +register_account "${XMPP_ADMIN_USER:-}" "${XMPP_ADMIN_PASSWORD:-}" "${XMPP_HOSTNAME:?}" + +exec runuser -u prosody -- "$@" diff --git a/generate-env.sh b/generate-env.sh index 11c9a790fd76aa0233c26b7468d08228ef829aba..625eee94da2ad64ae63af189b72352e28ded8a79 100644 --- a/generate-env.sh +++ b/generate-env.sh @@ -3,7 +3,7 @@ set -e FILE=${1:-.env} if [ -f "$FILE" ]; then - printf '\033[33mwarning: %d exists\033[0m\n' "$FILE" + printf '\033[33mwarning: %s exists\033[0m\n' "$FILE" exit 1 fi @@ -28,6 +28,14 @@ template() { add "MAILER_USERNAME" "" add "MAILER_PASSWORD" "" + section "at protocol" + add "PDS_JWT_SECRET" "$(hex_secret 16)" + add "PDS_ADMIN_PASSWORD" "$(secret 32)" + add "PDS_PLC_ROTATION_KEY_K256_PRIVATE_KEY_HEX" "$(pds_rotation_key)" + add "PDS_RATE_LIMITS_ENABLED" "true" + add "PDS_INVITE_REQUIRED" "false" + add "PDS_CONTACT_EMAIL_ADDRESS" "" + section "authentication" add "KEYCLOAK_ADMIN_PASSWORD" "$(secret 12)" @@ -49,6 +57,19 @@ template() { add "FORGEJO_SECURITY_SECRET_KEY" "$(forgejo generate secret SECRET_KEY)" add "FORGEJO_SECURITY_INTERNAL_TOKEN" "$(forgejo generate secret INTERNAL_TOKEN)" + section "tangled" + add "KNOT_SERVER_OWNER" "" + add "KNOT_APPVIEW_ENDPOINT" "https://tangled.org" + add "KNOT_SSH_PORT" "2222" + add "SPINDLE_SERVER_OWNER" "" + add "SPINDLE_IMAGE_TAG" "v1.14.0-alpha" + add "SPINDLE_NIXERY_PIPELINES_NIXERY" "nixery.tangled.sh" + add "SPINDLE_NIXERY_PIPELINES_WORKFLOW_TIMEOUT" "10m" + + section "xmpp" + add "XMPP_ADMIN_USER" "admin" + add "XMPP_ADMIN_PASSWORD" "$(secret 20)" + section "evil infra" add "EVIL_FORGEJO_SERVER_LFS_JWT_SECRET" "$(forgejo generate secret LFS_JWT_SECRET)" add "EVIL_FORGEJO_OAUTH2_JWT_SECRET" "$(forgejo generate secret JWT_SECRET)" @@ -57,6 +78,13 @@ template() { } secret() { openssl rand -base64 "$1" | tr -d '\n=/' | cut -c1-"$1"; } +hex_secret() { openssl rand -hex "$1"; } +pds_rotation_key() { + openssl ecparam -name secp256k1 -genkey -noout -outform DER | + tail -c +8 | + head -c 32 | + xxd -p -c 32 +} section() { printf '\n# %s\n' "$1" >>"$FILE"; } add() { printf '%s=%s\n' "$1" "${2:-}" >>"$FILE" -- 2.54.0