From c9a5cf6c35739d3415877afc0d8cb5254b0ffe8e Mon Sep 17 00:00:00 2001 From: Aykhan Shahsuvarov Date: Sat, 26 Apr 2025 21:55:59 +0400 Subject: [PATCH] init --- .github/workflows/publish-docker-image.yml | 47 +++++ Dockerfile | 12 ++ README.md | 60 ++++++ commands/account.sh | 231 +++++++++++++++++++++ commands/create-invite-code.sh | 14 ++ commands/help.sh | 41 ++++ commands/request-crawl.sh | 32 +++ pdsadmin.sh | 18 ++ 8 files changed, 455 insertions(+) create mode 100644 .github/workflows/publish-docker-image.yml create mode 100644 Dockerfile create mode 100644 README.md create mode 100644 commands/account.sh create mode 100644 commands/create-invite-code.sh create mode 100644 commands/help.sh create mode 100644 commands/request-crawl.sh create mode 100644 pdsadmin.sh diff --git a/.github/workflows/publish-docker-image.yml b/.github/workflows/publish-docker-image.yml new file mode 100644 index 0000000..9bcd923 --- /dev/null +++ b/.github/workflows/publish-docker-image.yml @@ -0,0 +1,47 @@ +name: publish-docker-image + +on: + push: + tags: + - "v*.*.*" + +jobs: + build-and-push-stable-image: + runs-on: ubuntu-latest + + permissions: + contents: read + packages: write + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to Docker Hub + uses: docker/login-action@v3 + with: + username: aykhans + password: ${{ secrets.DOCKER_TOKEN }} + + - name: Extract metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: aykhans/bsky-pdsadmin + tags: | + type=ref,event=tag + type=raw,value=latest + type=raw,value=stable + + - name: Build and Push + uses: docker/build-push-action@v6 + with: + context: ./ + file: ./Dockerfile + platforms: linux/amd64,linux/arm64 + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..176f2d3 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,12 @@ +FROM alpine:3.21.3 + +RUN apk add --no-cache bash curl jq util-linux openssl + +COPY ./pdsadmin.sh /usr/local/bin/pdsadmin +COPY ./commands /pdsadmin-commands + +RUN chmod +x /usr/local/bin/pdsadmin +RUN chmod +x /pdsadmin-commands/* + +ENTRYPOINT [ "/usr/local/bin/pdsadmin" ] +CMD [ "help" ] diff --git a/README.md b/README.md new file mode 100644 index 0000000..efd2e0a --- /dev/null +++ b/README.md @@ -0,0 +1,60 @@ +# Bluesky PDS Admin (Dockerized) + +This repository provides a Dockerized version of Bluesky's Personal Data Server (PDS) admin tool. +You can use all available commands from the original PDS admin tool, **except** for the `update` command. + +## Usage Examples + +```sh +docker run -it --rm aykhans/bsky-pdsadmin help +``` + +### Account Management + +**List accounts:** + +```sh +docker run -it --rm --env-file /pds/pds.env aykhans/bsky-pdsadmin account list +``` + +**Create an account:** + +```sh +docker run -it --rm --env-file /pds/pds.env aykhans/bsky-pdsadmin account create +``` + +**Delete an account:** + +```sh +docker run -it --rm --env-file /pds/pds.env aykhans/bsky-pdsadmin account delete +``` + +**Takedown an account:** + +```sh +docker run -it --rm --env-file /pds/pds.env aykhans/bsky-pdsadmin account takedown +``` + +**Untakedown an account:** + +```sh +docker run -it --rm --env-file /pds/pds.env aykhans/bsky-pdsadmin account untakedown +``` + +**Reset an account password:** + +```sh +docker run -it --rm --env-file /pds/pds.env aykhans/bsky-pdsadmin account reset-password +``` + +### Request a Crawl + +```sh +docker run --rm --env-file /pds/pds.env aykhans/bsky-pdsadmin request-crawl +``` + +### Create an Invite Code + +```sh +docker run --rm --env-file /pds/pds.env aykhans/bsky-pdsadmin create-invite-code +``` diff --git a/commands/account.sh b/commands/account.sh new file mode 100644 index 0000000..4c9e032 --- /dev/null +++ b/commands/account.sh @@ -0,0 +1,231 @@ +#!/bin/bash +set -o errexit +set -o nounset +set -o pipefail + +# curl a URL and fail if the request fails. +function curl_cmd_get { + curl --fail --silent --show-error "$@" +} + +# curl a URL and fail if the request fails. +function curl_cmd_post { + curl --fail --silent --show-error --request POST --header "Content-Type: application/json" "$@" +} + +# curl a URL but do not fail if the request fails. +function curl_cmd_post_nofail { + curl --silent --show-error --request POST --header "Content-Type: application/json" "$@" +} + +# The subcommand to run. +SUBCOMMAND="${1:-}" + +# +# account list +# +if [[ "${SUBCOMMAND}" == "list" ]]; then + DIDS="$(curl_cmd_get \ + "https://${PDS_HOSTNAME}/xrpc/com.atproto.sync.listRepos?limit=100" | jq --raw-output '.repos[].did' + )" + OUTPUT='[{"handle":"Handle","email":"Email","did":"DID"}' + for did in ${DIDS}; do + ITEM="$(curl_cmd_get \ + --user "admin:${PDS_ADMIN_PASSWORD}" \ + "https://${PDS_HOSTNAME}/xrpc/com.atproto.admin.getAccountInfo?did=${did}" + )" + OUTPUT="${OUTPUT},${ITEM}" + done + OUTPUT="${OUTPUT}]" + echo "${OUTPUT}" | jq --raw-output '.[] | [.handle, .email, .did] | @tsv' | column --table + +# +# account create +# +elif [[ "${SUBCOMMAND}" == "create" ]]; then + EMAIL="${2:-}" + HANDLE="${3:-}" + + if [[ "${EMAIL}" == "" ]]; then + read -p "Enter an email address (e.g. alice@${PDS_HOSTNAME}): " EMAIL + fi + if [[ "${HANDLE}" == "" ]]; then + read -p "Enter a handle (e.g. alice.${PDS_HOSTNAME}): " HANDLE + fi + + if [[ "${EMAIL}" == "" || "${HANDLE}" == "" ]]; then + echo "ERROR: missing EMAIL and/or HANDLE parameters." >/dev/stderr + echo "Usage: $0 ${SUBCOMMAND} " >/dev/stderr + exit 1 + fi + + PASSWORD="$(openssl rand -base64 30 | tr -d "=+/" | cut -c1-24)" + INVITE_CODE="$(curl_cmd_post \ + --user "admin:${PDS_ADMIN_PASSWORD}" \ + --data '{"useCount": 1}' \ + "https://${PDS_HOSTNAME}/xrpc/com.atproto.server.createInviteCode" | jq --raw-output '.code' + )" + RESULT="$(curl_cmd_post_nofail \ + --data "{\"email\":\"${EMAIL}\", \"handle\":\"${HANDLE}\", \"password\":\"${PASSWORD}\", \"inviteCode\":\"${INVITE_CODE}\"}" \ + "https://${PDS_HOSTNAME}/xrpc/com.atproto.server.createAccount" + )" + + DID="$(echo $RESULT | jq --raw-output '.did')" + if [[ "${DID}" != did:* ]]; then + ERR="$(echo ${RESULT} | jq --raw-output '.message')" + echo "ERROR: ${ERR}" >/dev/stderr + echo "Usage: $0 ${SUBCOMMAND} " >/dev/stderr + exit 1 + fi + + echo + echo "Account created successfully!" + echo "-----------------------------" + echo "Handle : ${HANDLE}" + echo "DID : ${DID}" + echo "Password : ${PASSWORD}" + echo "-----------------------------" + echo "Save this password, it will not be displayed again." + echo + +# +# account delete +# +elif [[ "${SUBCOMMAND}" == "delete" ]]; then + DID="${2:-}" + + if [[ "${DID}" == "" ]]; then + echo "ERROR: missing DID parameter." >/dev/stderr + echo "Usage: $0 ${SUBCOMMAND} " >/dev/stderr + exit 1 + fi + + if [[ "${DID}" != did:* ]]; then + echo "ERROR: DID parameter must start with \"did:\"." >/dev/stderr + echo "Usage: $0 ${SUBCOMMAND} " >/dev/stderr + exit 1 + fi + + echo "This action is permanent." + read -r -p "Are you sure you'd like to delete ${DID}? [y/N] " response + if [[ ! "${response}" =~ ^([yY][eE][sS]|[yY])$ ]]; then + exit 0 + fi + + curl_cmd_post \ + --user "admin:${PDS_ADMIN_PASSWORD}" \ + --data "{\"did\": \"${DID}\"}" \ + "https://${PDS_HOSTNAME}/xrpc/com.atproto.admin.deleteAccount" >/dev/null + + echo "${DID} deleted" + +# +# account takedown +# +elif [[ "${SUBCOMMAND}" == "takedown" ]]; then + DID="${2:-}" + TAKEDOWN_REF="$(date +%s)" + + if [[ "${DID}" == "" ]]; then + echo "ERROR: missing DID parameter." >/dev/stderr + echo "Usage: $0 ${SUBCOMMAND} " >/dev/stderr + exit 1 + fi + + if [[ "${DID}" != did:* ]]; then + echo "ERROR: DID parameter must start with \"did:\"." >/dev/stderr + echo "Usage: $0 ${SUBCOMMAND} " >/dev/stderr + exit 1 + fi + + PAYLOAD="$(cat </dev/null + + echo "${DID} taken down" + +# +# account untakedown +# +elif [[ "${SUBCOMMAND}" == "untakedown" ]]; then + DID="${2:-}" + + if [[ "${DID}" == "" ]]; then + echo "ERROR: missing DID parameter." >/dev/stderr + echo "Usage: $0 ${SUBCOMMAND} " >/dev/stderr + exit 1 + fi + + if [[ "${DID}" != did:* ]]; then + echo "ERROR: DID parameter must start with \"did:\"." >/dev/stderr + echo "Usage: $0 ${SUBCOMMAND} " >/dev/stderr + exit 1 + fi + + PAYLOAD=$(cat </dev/null + + echo "${DID} untaken down" +# +# account reset-password +# +elif [[ "${SUBCOMMAND}" == "reset-password" ]]; then + DID="${2:-}" + PASSWORD="$(openssl rand -base64 30 | tr -d "=+/" | cut -c1-24)" + + if [[ "${DID}" == "" ]]; then + echo "ERROR: missing DID parameter." >/dev/stderr + echo "Usage: $0 ${SUBCOMMAND} " >/dev/stderr + exit 1 + fi + + if [[ "${DID}" != did:* ]]; then + echo "ERROR: DID parameter must start with \"did:\"." >/dev/stderr + echo "Usage: $0 ${SUBCOMMAND} " >/dev/stderr + exit 1 + fi + + curl_cmd_post \ + --user "admin:${PDS_ADMIN_PASSWORD}" \ + --data "{ \"did\": \"${DID}\", \"password\": \"${PASSWORD}\" }" \ + "https://${PDS_HOSTNAME}/xrpc/com.atproto.admin.updateAccountPassword" >/dev/null + + echo + echo "Password reset for ${DID}" + echo "New password: ${PASSWORD}" + echo + +else + echo "Unknown subcommand: ${SUBCOMMAND}" >/dev/stderr + exit 1 +fi diff --git a/commands/create-invite-code.sh b/commands/create-invite-code.sh new file mode 100644 index 0000000..eafe46d --- /dev/null +++ b/commands/create-invite-code.sh @@ -0,0 +1,14 @@ +#!/bin/bash +set -o errexit +set -o nounset +set -o pipefail + +curl \ + --fail \ + --silent \ + --show-error \ + --request POST \ + --user "admin:${PDS_ADMIN_PASSWORD}" \ + --header "Content-Type: application/json" \ + --data '{"useCount": 1}' \ + "https://${PDS_HOSTNAME}/xrpc/com.atproto.server.createInviteCode" | jq --raw-output '.code' \ No newline at end of file diff --git a/commands/help.sh b/commands/help.sh new file mode 100644 index 0000000..431df9b --- /dev/null +++ b/commands/help.sh @@ -0,0 +1,41 @@ +#!/bin/bash +set -o errexit +set -o nounset +set -o pipefail + +# This script is used to display help information for the pdsadmin command. +cat < + Create a new account + e.g. pdsadmin account create alice@example.com alice.example.com + delete + Delete an account specified by DID. + e.g. pdsadmin account delete did:plc:xyz123abc456 + takedown + Takedown an account specified by DID. + e.g. pdsadmin account takedown did:plc:xyz123abc456 + untakedown + Remove a takedown from an account specified by DID. + e.g. pdsadmin account untakedown did:plc:xyz123abc456 + reset-password + Reset a password for an account specified by DID. + e.g. pdsadmin account reset-password did:plc:xyz123abc456 + +request-crawl [] + Request a crawl from a relay host. + e.g. pdsadmin request-crawl bsky.network + +create-invite-code + Create a new invite code. + e.g. pdsadmin create-invite-code + +help + Display this help information. + +HELP \ No newline at end of file diff --git a/commands/request-crawl.sh b/commands/request-crawl.sh new file mode 100644 index 0000000..4903e4a --- /dev/null +++ b/commands/request-crawl.sh @@ -0,0 +1,32 @@ +#!/bin/bash +set -o errexit +set -o nounset +set -o pipefail + +RELAY_HOSTS="${1:-}" +if [[ "${RELAY_HOSTS}" == "" ]]; then + RELAY_HOSTS="${PDS_CRAWLERS}" +fi + +if [[ "${RELAY_HOSTS}" == "" ]]; then + echo "ERROR: missing RELAY HOST parameter." >/dev/stderr + echo "Usage: $0 [,,...]" >/dev/stderr + exit 1 +fi + +for host in ${RELAY_HOSTS//,/ }; do + echo "Requesting crawl from ${host}" + if [[ $host != https:* && $host != http:* ]]; then + host="https://${host}" + fi + curl \ + --fail \ + --silent \ + --show-error \ + --request POST \ + --header "Content-Type: application/json" \ + --data "{\"hostname\": \"${PDS_HOSTNAME}\"}" \ + "${host}/xrpc/com.atproto.sync.requestCrawl" >/dev/null +done + +echo "done" \ No newline at end of file diff --git a/pdsadmin.sh b/pdsadmin.sh new file mode 100644 index 0000000..bd1250d --- /dev/null +++ b/pdsadmin.sh @@ -0,0 +1,18 @@ +#!/bin/bash +set -o errexit +set -o nounset +set -o pipefail + +# Command to run. +COMMAND="${1:-help}" +shift || true + +# Download the script, if it exists. +SCRIPT_FILE="/pdsadmin-commands/${COMMAND}.sh" + +if [ ! -f "${SCRIPT_FILE}" ]; then + echo "ERROR: ${COMMAND} not found" + exit 2 +fi + +"${SCRIPT_FILE}" "$@"