This commit is contained in:
Aykhan Shahsuvarov 2025-04-26 21:55:59 +04:00
commit c9a5cf6c35
8 changed files with 455 additions and 0 deletions

View File

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

12
Dockerfile Normal file
View File

@ -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" ]

60
README.md Normal file
View File

@ -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 <DID>
```
**Takedown an account:**
```sh
docker run -it --rm --env-file /pds/pds.env aykhans/bsky-pdsadmin account takedown <DID>
```
**Untakedown an account:**
```sh
docker run -it --rm --env-file /pds/pds.env aykhans/bsky-pdsadmin account untakedown <DID>
```
**Reset an account password:**
```sh
docker run -it --rm --env-file /pds/pds.env aykhans/bsky-pdsadmin account reset-password <DID>
```
### 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
```

231
commands/account.sh Normal file
View File

@ -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} <EMAIL> <HANDLE>" >/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} <EMAIL> <HANDLE>" >/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} <DID>" >/dev/stderr
exit 1
fi
if [[ "${DID}" != did:* ]]; then
echo "ERROR: DID parameter must start with \"did:\"." >/dev/stderr
echo "Usage: $0 ${SUBCOMMAND} <DID>" >/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} <DID>" >/dev/stderr
exit 1
fi
if [[ "${DID}" != did:* ]]; then
echo "ERROR: DID parameter must start with \"did:\"." >/dev/stderr
echo "Usage: $0 ${SUBCOMMAND} <DID>" >/dev/stderr
exit 1
fi
PAYLOAD="$(cat <<EOF
{
"subject": {
"\$type": "com.atproto.admin.defs#repoRef",
"did": "${DID}"
},
"takedown": {
"applied": true,
"ref": "${TAKEDOWN_REF}"
}
}
EOF
)"
curl_cmd_post \
--user "admin:${PDS_ADMIN_PASSWORD}" \
--data "${PAYLOAD}" \
"https://${PDS_HOSTNAME}/xrpc/com.atproto.admin.updateSubjectStatus" >/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} <DID>" >/dev/stderr
exit 1
fi
if [[ "${DID}" != did:* ]]; then
echo "ERROR: DID parameter must start with \"did:\"." >/dev/stderr
echo "Usage: $0 ${SUBCOMMAND} <DID>" >/dev/stderr
exit 1
fi
PAYLOAD=$(cat <<EOF
{
"subject": {
"\$type": "com.atproto.admin.defs#repoRef",
"did": "${DID}"
},
"takedown": {
"applied": false
}
}
EOF
)
curl_cmd_post \
--user "admin:${PDS_ADMIN_PASSWORD}" \
--data "${PAYLOAD}" \
"https://${PDS_HOSTNAME}/xrpc/com.atproto.admin.updateSubjectStatus" >/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} <DID>" >/dev/stderr
exit 1
fi
if [[ "${DID}" != did:* ]]; then
echo "ERROR: DID parameter must start with \"did:\"." >/dev/stderr
echo "Usage: $0 ${SUBCOMMAND} <DID>" >/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

View File

@ -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'

41
commands/help.sh Normal file
View File

@ -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 <<HELP
pdsadmin help
--
account
list
List accounts
e.g. pdsadmin account list
create <EMAIL> <HANDLE>
Create a new account
e.g. pdsadmin account create alice@example.com alice.example.com
delete <DID>
Delete an account specified by DID.
e.g. pdsadmin account delete did:plc:xyz123abc456
takedown <DID>
Takedown an account specified by DID.
e.g. pdsadmin account takedown did:plc:xyz123abc456
untakedown <DID>
Remove a takedown from an account specified by DID.
e.g. pdsadmin account untakedown did:plc:xyz123abc456
reset-password <DID>
Reset a password for an account specified by DID.
e.g. pdsadmin account reset-password did:plc:xyz123abc456
request-crawl [<RELAY HOST>]
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

32
commands/request-crawl.sh Normal file
View File

@ -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 <RELAY HOST>[,<RELAY HOST>,...]" >/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"

18
pdsadmin.sh Normal file
View File

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