#!/bin/sh # Send a Nowlark notification from anything that is not Python. # # nowlark [title] [body] [--url URL] [--priority N] [--group G] # [--dedupe D] [--replace R] [--check] [--quiet] # nowlark --register # # The key comes from NOWLARK_API_KEY, or from the crontab, the same way the # Python client finds it: a launchd agent does not inherit cron's environment. # # is the sender's name on the phone and its row in Sources: one per # script or job, the same on every run, never a shared machine name. # # --register lists in the app now, without sending anything: for a # job that only speaks when it fails. Registering an existing name is a no-op. # # Exits 0 when the server accepted it, 1 when it refused, 2 on a usage error. # A duplicate and a muted source are acceptances, not failures. set -eu BASE_URL="${NOWLARK_BASE_URL:-https://nowlark.com}" TIMEOUT="${NOWLARK_TIMEOUT:-20}" usage() { sed -n '2,18p' "$0" | sed 's/^# \{0,1\}//' exit 2 } api_key() { if [ -n "${NOWLARK_API_KEY:-}" ]; then printf '%s' "$NOWLARK_API_KEY" return 0 fi /usr/bin/crontab -l 2>/dev/null \ | sed -n 's/^[[:space:]]*NOWLARK_API_KEY[[:space:]]*=[[:space:]]*['"'"'"]\{0,1\}\([^'"'"'" ]*\).*/\1/p' \ | head -1 } # JSON-escape a string using the json module rather than sed, so a description # containing a quote, a backslash or a newline does not produce invalid JSON. json_string() { printf '%s' "$1" | /usr/bin/python3 -c 'import json,sys; print(json.dumps(sys.stdin.read()))' } SOURCE=""; TITLE=""; BODY=""; URL=""; PRIORITY=0; GROUP=""; DEDUPE=""; REPLACE="" CHECK=0; QUIET=0; REGISTER=0 while [ $# -gt 0 ]; do case "$1" in --url) URL="${2:?--url needs a value}"; shift 2 ;; --priority) PRIORITY="${2:?--priority needs a value}"; shift 2 ;; --group) GROUP="${2:?--group needs a value}"; shift 2 ;; --dedupe) DEDUPE="${2:?--dedupe needs a value}"; shift 2 ;; --replace) REPLACE="${2:?--replace needs a value}"; shift 2 ;; --check) CHECK=1; shift ;; --register) REGISTER=1; shift ;; --quiet) QUIET=1; shift ;; -h|--help) usage ;; -*) echo "nowlark: unknown option $1" >&2; usage ;; *) if [ -z "$SOURCE" ]; then SOURCE="$1" elif [ -z "$TITLE" ]; then TITLE="$1" elif [ -z "$BODY" ]; then BODY="$1" else echo "nowlark: too many arguments" >&2; usage fi shift ;; esac done KEY=$(api_key) if [ -z "$KEY" ]; then echo "nowlark: NOWLARK_API_KEY is not set" >&2 exit 2 fi if [ "$CHECK" -eq 1 ]; then curl -sS --max-time "$TIMEOUT" -f \ -H "X-Nowlark-Key: $KEY" "$BASE_URL/v1/key/check" printf '\n' exit 0 fi [ -n "$SOURCE" ] || usage if [ "$REGISTER" -eq 1 ]; then STATUS=$(curl -sS --max-time "$TIMEOUT" -o /dev/null -w '%{http_code}' \ -H "X-Nowlark-Key: $KEY" -H 'Content-Type: application/json' \ -d "{\"source\":$(json_string "$SOURCE")}" "$BASE_URL/v1/sources" 2>/dev/null) || STATUS=000 if [ "$STATUS" = "200" ]; then [ "$QUIET" -eq 1 ] || echo "registered $SOURCE" exit 0 fi [ "$QUIET" -eq 1 ] || echo "nowlark: could not register (HTTP $STATUS)" >&2 exit 1 fi [ -n "$TITLE" ] || [ -n "$BODY" ] || { echo "nowlark: a title or a body is required" >&2; exit 2; } PAYLOAD="{\"source\":$(json_string "$SOURCE"),\"priority\":$PRIORITY" [ -n "$TITLE" ] && PAYLOAD="$PAYLOAD,\"title\":$(json_string "$TITLE")" [ -n "$BODY" ] && PAYLOAD="$PAYLOAD,\"body\":$(json_string "$BODY")" [ -n "$URL" ] && PAYLOAD="$PAYLOAD,\"url\":$(json_string "$URL")" [ -n "$GROUP" ] && PAYLOAD="$PAYLOAD,\"group_key\":$(json_string "$GROUP")" [ -n "$DEDUPE" ] && PAYLOAD="$PAYLOAD,\"dedupe_key\":$(json_string "$DEDUPE")" [ -n "$REPLACE" ] && PAYLOAD="$PAYLOAD,\"replace_key\":$(json_string "$REPLACE")" PAYLOAD="$PAYLOAD}" RESPONSE=$(curl -sS --max-time "$TIMEOUT" -w '\n%{http_code}' \ -H "X-Nowlark-Key: $KEY" -H 'Content-Type: application/json' \ -d "$PAYLOAD" "$BASE_URL/v1/notify" 2>&1) || { [ "$QUIET" -eq 1 ] || echo "nowlark: could not reach $BASE_URL" >&2 exit 1 } STATUS=$(printf '%s' "$RESPONSE" | tail -1) BODY_OUT=$(printf '%s' "$RESPONSE" | sed '$d') if [ "$STATUS" = "200" ]; then [ "$QUIET" -eq 1 ] || printf '%s\n' "$BODY_OUT" exit 0 fi [ "$QUIET" -eq 1 ] || printf 'nowlark: refused (HTTP %s) %s\n' "$STATUS" "$BODY_OUT" >&2 exit 1