#!/bin/sh
#
# 0wx.sh -- a command line client for the 0wx API.
#
#     ./0wx.sh <action> [--parameter value ...]
#     ./0wx.sh help
#
# This script is meant to be read before it is run. It is plain POSIX shell,
# it needs nothing but curl, and every request it makes is one you could type
# yourself. If anything here looks wrong to you, check it against the API
# documentation on the site's "API" page.
#
# It is deliberately not clever: no config file, no dependency on jq, and no
# attempt to reformat the answer. You get the server's JSON, unchanged, on
# standard output.

set -eu

# ---------------------------------------------------------------------------
# Settings
# ---------------------------------------------------------------------------
# Either may come from the environment, which is the least surprising way to
# keep a key out of your shell history:
#
#     export OWX_KEY=...
#     ./0wx.sh listfiles
#
OWX_URL="${OWX_URL:-https://0wx.es/api.cgi}"
OWX_KEY="${OWX_KEY:-}"

usage() {
    cat <<'EOF'
0wx.sh -- command line client for the 0wx API

USAGE
    ./0wx.sh <action> [--parameter value ...]
    ./0wx.sh help

SETTINGS
    OWX_URL   API endpoint   (default https://0wx.es/api.cgi)
    OWX_KEY   your API key   (generate one on your account page)

    Both can also be given as --url and --key. Using the environment keeps
    the key out of your shell history.

NO KEY NEEDED
    whoami                                  check a key, read the limits
    ip                                      how your address looks from here
    tinyurl   --url URL                     shorten a URL
    hugeurl   --url URL                     lengthen a URL, absurdly
    paste     --content TEXT                store text, get a link
    translate --content TEXT --to LANG      translate text
    upload    --file PATH                   upload a file

KEY NEEDED
    listfiles     [--page N] [--g ID]       your files; --g none = ungrouped
    listpastes    [--page N]                your pastes
    listurls      [--page N]                your short and huge URLs
    listgalleries                           your galleries
    listotl       [--page N]                your one-time links

    upload    --file PATH --gallery NAME    upload into a gallery
    otl       --share SHARE                 mint a one-time link
    move      --share SHARE --gallery NAME  move files into a gallery
    move      --share SHARE                 move them back out
    gallery   --g ID --publish true|false   publish or withdraw a gallery
    delete    --share SHARE                 delete files, pastes or URLs
    delete    --g ID [--withfiles true]     delete a gallery
    delete    --otl TOKEN                   kill a one-time link

REPEATING A PARAMETER
    --file, --share, --g and --otl may be given more than once:

        ./0wx.sh delete --share Ab3xK9pQ --share Zz7yT2mN
        ./0wx.sh upload --file one.png --file two.png

EXAMPLES
    ./0wx.sh paste --content 'hello & world'
    ./0wx.sh upload --file holiday.jpg --gallery 'Holiday 2026'
    ./0wx.sh listfiles --page 2
    ./0wx.sh gallery --g 7 --publish true
    ./0wx.sh delete --share Ab3xK9pQ --otl Kk1pR8wZ

NOTES
    Values are sent with curl's --data-urlencode, so an ampersand or a space
    in your text is safe. You quote for your shell, not for the server.

    If jq is installed the reply is laid out for reading; if not, it is
    printed exactly as the server sent it. Either way it is the server's
    JSON and nothing else, so piping this into another program is safe.
EOF
}

die() {
    echo "0wx.sh: $1" >&2
    exit 1
}

# ---------------------------------------------------------------------------
# The action
# ---------------------------------------------------------------------------
[ $# -gt 0 ] || { usage; exit 0; }

action="$1"
shift

case "$action" in
    help|--help|-h) usage; exit 0 ;;
esac

case "$action" in
    -*) die "the first argument must be an action, not '$action'. Try: ./0wx.sh help" ;;
esac

# ---------------------------------------------------------------------------
# Building the request
# ---------------------------------------------------------------------------
# curl arguments are accumulated in the positional parameters. POSIX shell
# has no arrays, and "$@" is the one construct that carries values with
# spaces or quotes in them through to curl unmangled.
#
# The caller's own arguments are read first and stashed in variables, so that
# "set --" can then start a fresh list without losing them.

files=''        # newline-separated paths, because --file may repeat
fields=''       # newline-separated name=value, likewise

while [ $# -gt 0 ]; do
    case "$1" in
        --url)  [ $# -ge 2 ] || die "--url needs a value";  OWX_URL="$2"; shift 2 ;;
        --key)  [ $# -ge 2 ] || die "--key needs a value";  OWX_KEY="$2"; shift 2 ;;

        --file)
            [ $# -ge 2 ] || die "--file needs a path"
            [ -r "$2" ]  || die "cannot read file: $2"
            files="$files$2
"
            shift 2
            ;;

        --*)
            [ $# -ge 2 ] || die "$1 needs a value"
            # Strip the leading dashes: --share becomes share.
            name="${1#--}"
            fields="$fields$name=$2
"
            shift 2
            ;;

        *)  die "unexpected argument '$1'. Parameters look like --name value." ;;
    esac
done

# Now the curl argument list, starting empty.
set --

# Quiet, but still report a failed connection, and follow nothing: the API
# never redirects, and a client that follows redirects can be walked
# somewhere else by a compromised server.
set -- --silent --show-error --max-redirs 0

[ -n "$OWX_KEY" ] && set -- "$@" --header "Authorization: Bearer $OWX_KEY"

# The action is a field like any other, so it uses the same encoding.
if [ -n "$files" ]; then
    set -- "$@" --form-string "action=$action"
else
    set -- "$@" --data-urlencode "action=$action"
fi

# One encoding for the whole request, chosen by whether a file is involved.
#
# curl REFUSES to mix --data-urlencode with --form -- it exits 2 with "you
# can only select one HTTP request method" -- so an upload has to send its
# other fields as form parts too. A multipart part carries its value as-is,
# with a boundary rather than an "&" between fields, so nothing needs
# encoding there.
#
# Without a file, --data-urlencode for every value, not only the ones that
# look risky: the server splits form fields on "&", so one unencoded
# ampersand in a paste or a gallery name silently truncates it, and encoding
# something that did not need it costs nothing.
#
# The IFS dance is how POSIX shell walks newline-separated text without also
# splitting on the spaces inside the values.
if [ -n "$files" ]; then
    # --form-string, NOT --form. With --form, a value beginning with "@" or
    # "<" makes curl read a local file and send its contents instead -- so
    # a gallery called "@notes.txt" would upload that file, and a gallery
    # called "@/etc/passwd" would upload that. --form-string sends the value
    # literally and has no such syntax.
    encode_as=--form-string
else
    encode_as=--data-urlencode
fi

if [ -n "$fields" ]; then
    OLD_IFS="$IFS"
    IFS='
'
    for pair in $fields; do
        IFS="$OLD_IFS"
        set -- "$@" "$encode_as" "$pair"
        IFS='
'
    done
    IFS="$OLD_IFS"
fi

if [ -n "$files" ]; then
    OLD_IFS="$IFS"
    IFS='
'
    for path in $files; do
        IFS="$OLD_IFS"
        set -- "$@" --form "file=@$path"
        IFS='
'
    done
    IFS="$OLD_IFS"
fi

command -v curl >/dev/null 2>&1 || die "curl is not installed"

# ---------------------------------------------------------------------------
# Send it
# ---------------------------------------------------------------------------
# With jq installed the answer is laid out; without it, printed as it came.
#
# Not written as "exec curl ... | jq ." for two reasons:
#
#   A pipeline reports the status of its LAST command. curl writes its
#   errors to stderr and nothing to stdout, so jq would read empty input,
#   exit 0, and a failed request would look like a success -- breaking
#   "./0wx.sh ... && something".
#
#   jq only prints what it can parse. The API always answers JSON, but a
#   proxy timeout or a block page in front of it does not, and jq would
#   replace that page with a parse error -- losing the one thing worth
#   reading at exactly the moment you need it.
#
# So: run curl, keep its status, and pretty-print only if that works.
if command -v jq >/dev/null 2>&1; then
    body=$(curl "$@" "$OWX_URL")
    status=$?

    if [ -n "$body" ]; then
        printf '%s\n' "$body" | jq . 2>/dev/null || printf '%s\n' "$body"
    fi

    exit $status
fi

exec curl "$@" "$OWX_URL"
