#!/usr/bin/env bash # # dropkind — send articles and documents to your Kindle from the terminal. # # Install: # curl -fsSL https://dropkind.app/install.sh | sh # # Usage: # dropkind login store your API token (get one at https://dropkind.app/settings/api) # dropkind send deliver a web article or a document # dropkind send - [--title T] deliver plain text read from stdin # dropkind status check a drop # dropkind logout forget the stored token # # Environment: # DROPKIND_TOKEN use this token instead of the stored one # DROPKIND_URL API base URL (default: https://dropkind.app) set -euo pipefail VERSION="0.1.1" BASE_URL="${DROPKIND_URL:-https://dropkind.app}" CONFIG_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/dropkind" TOKEN_FILE="$CONFIG_DIR/token" die() { echo "dropkind: $*" >&2 exit 1 } usage() { sed -n 's/^# \{0,1\}//p' "$0" | sed -n '2,17p' } token() { if [ -n "${DROPKIND_TOKEN:-}" ]; then echo "$DROPKIND_TOKEN" elif [ -f "$TOKEN_FILE" ]; then cat "$TOKEN_FILE" else die "no API token. Run 'dropkind login' first (get a token at $BASE_URL/settings/api)" fi } # json_get — extract a top-level field from JSON on stdin. json_get() { if command -v python3 >/dev/null 2>&1; then python3 -c 'import json,sys try: value = json.load(sys.stdin).get(sys.argv[1]) except Exception: value = None print("" if value is None else value)' "$1" else sed -n "s/.*\"$1\":[[:space:]]*\"\([^\"]*\)\".*/\1/p; s/.*\"$1\":[[:space:]]*\([0-9][0-9]*\).*/\1/p" | head -1 fi } # api_request [curl args...] — sets STATUS and BODY. api_request() { local method="$1" path="$2" tmp shift 2 tmp="$(mktemp)" STATUS="$(curl -sS -o "$tmp" -w '%{http_code}' -X "$method" \ -H "Authorization: Bearer $(token)" \ "$@" "$BASE_URL$path")" || { rm -f "$tmp"; die "could not reach $BASE_URL"; } BODY="$(cat "$tmp")" rm -f "$tmp" } fail_from_response() { local fallback="$1" message message="$(printf '%s' "$BODY" | json_get error)" case "$STATUS" in 401) die "invalid or expired token. Run 'dropkind login' with a fresh token from $BASE_URL/settings/api" ;; 403) die "${message:-account setup incomplete. Finish onboarding at $BASE_URL first}" ;; 429) die "rate limited — try again in a minute" ;; *) die "${message:-$fallback (HTTP $STATUS)}" ;; esac } cmd_login() { local input printf 'Paste your API token (from %s/settings/api): ' "$BASE_URL" read -rs input echo [ -n "$input" ] || die "no token entered" # Probing drop 0: a valid token earns a JSON not_found, a bad one a 401, # and a server without the API an HTML 404. local tmp tmp="$(mktemp)" STATUS="$(curl -sS -o "$tmp" -w '%{http_code}' \ -H "Authorization: Bearer $input" \ "$BASE_URL/api/v1/drops/0")" || { rm -f "$tmp"; die "could not reach $BASE_URL"; } BODY="$(cat "$tmp")" rm -f "$tmp" case "$STATUS" in 401) die "that token was not accepted" ;; 404) printf '%s' "$BODY" | grep -q not_found || die "no DropKind API at $BASE_URL (HTTP 404)" ;; esac mkdir -p "$CONFIG_DIR" printf '%s' "$input" > "$TOKEN_FILE" chmod 600 "$TOKEN_FILE" echo "Token saved to $TOKEN_FILE" } cmd_logout() { rm -f "$TOKEN_FILE" echo "Token removed" } cmd_send() { local target="" title="" id status while [ $# -gt 0 ]; do case "$1" in -t|--title) title="${2:-}"; shift 2 ;; -?*) die "unknown option: $1" ;; *) [ -z "$target" ] || die "only one URL or file per send"; target="$1"; shift ;; esac done [ -n "$target" ] || die "usage: dropkind send [--title TITLE]" local -a form if [ "$target" = "-" ]; then local text text="$(cat)" [ -n "$text" ] || die "nothing on stdin to send" form=(--form-string "text=$text") elif [ -f "$target" ]; then form=(-F "file=@$target") elif printf '%s' "$target" | grep -qiE '^https?://'; then form=(--form-string "url=$target") else die "'$target' is neither an existing file, an http(s) URL, nor '-' for stdin" fi [ -z "$title" ] || form+=(--form-string "title=$title") api_request POST /api/v1/drops "${form[@]}" [ "$STATUS" = "201" ] || fail_from_response "drop was not accepted" id="$(printf '%s' "$BODY" | json_get id)" echo "Drop #$id queued. Check it with: dropkind status $id" } cmd_status() { local id="${1:-}" status title sent_at web_url detail [ -n "$id" ] || die "usage: dropkind status " api_request GET "/api/v1/drops/$id" [ "$STATUS" = "200" ] || fail_from_response "drop not found" status="$(printf '%s' "$BODY" | json_get status)" title="$(printf '%s' "$BODY" | json_get title)" sent_at="$(printf '%s' "$BODY" | json_get sent_at)" echo "Drop #$id: $status${title:+ — $title}${sent_at:+ (sent $sent_at)}" case "$status" in needs_action|failed) detail="$(printf '%s' "$BODY" | json_get detail)" web_url="$(printf '%s' "$BODY" | json_get web_url)" if [ -n "$detail" ]; then echo " $detail" elif [ -n "$web_url" ]; then echo " see $web_url" fi ;; esac } case "${1:-}" in login) shift; cmd_login "$@" ;; logout) shift; cmd_logout "$@" ;; send) shift; cmd_send "$@" ;; status) shift; cmd_status "$@" ;; -v|--version|version) echo "dropkind $VERSION" ;; ""|-h|--help|help) usage ;; *) die "unknown command: $1 (try 'dropkind help')" ;; esac