01 / apply it
yardr pack apply pushover --source-env pushover=<file> --notify <user>=pushover,<file>yardr carries this pack in its binary; the name is enough. yardr pack check pushover shows what it would change first. It goes into the workspace itself, and into no repo · depot.
yardr pack apply https://yardr.dev/packs/pushover/ --source-env pushover=<file> --notify <user>=pushover,<file>From this site, as the files below.
02 / the files
Every file, as it ships.
# Pushover as a pack of kind source: the integration, imported into the
# yard and into no depot. What Pushover is here: one push to one person,
# which the yard sends through the call the declaration names, to the
# user key of the person told.
#
# yardr pack apply pushover --source-env pushover=<file> --notify <user>=pushover,<file>
#
# The source is named pushover: yardr knows Pushover as this pack alone.
# The declaration is the file docs/pushover.md has, to the byte, with one
# line more: notify's image, which names the command beside it that sends
# a picture (the page's file has none, for there is no command by hand).
name: pushover
kind: source
setup: SETUP.md
sources:
pushover:
declaration: pushover.yaml
#!/bin/bash
# notify: what the yard tells a person through Pushover when the message
# carries a picture, for the notify key's image command. The verb is tell:
# one JSON object on stdin, {address, title, text, image}, and one on
# stdout, {status}. PUSHOVER_TOKEN and YARDR_SOURCE_BASE, the declaration's
# base, are in the environment. Needs curl, jq, file and the standard shell
# utilities it calls (tr, wc).
#
# The declaration's message call sends JSON, which carries no picture, so a
# message with one is sent here as the multipart form the API takes for an
# attachment. A message takes one picture of 5,242,880 bytes at most, and
# fitting it under that is the sender's work (pushover.net/api, read 22
# October 2026): a larger one is refused here, with its size, before
# anything is sent, and the daemon tells the words alone.
set -euo pipefail
in=$(cat)
arg() { jq -r --arg k "$1" '.[$k] // ""' <<<"$in"; }
cfgq() { # a value for a line of curl's config, quoted: a " or \ or a line break in it is data, never the config's syntax or another option
local s=${1//\\/\\\\}
s=${s//\"/\\\"}
s=${s//$'\n'/\\n}
s=${s//$'\r'/\\r}
printf '"%s"' "$s"
}
qform() { # a value for curl's -F, quoted, so that a ; or , or " in a file's name is no part of the option's syntax
local s=${1//\\/\\\\}
printf '"%s"' "${s//\"/\\\"}"
}
clip() { # a text cut to n characters, not bytes, with the ellipsis the declared call's cut leaves
jq -nr --arg s "$1" --argjson n "$2" '$s | if length > $n then .[0:$n-1] + "…" else . end'
}
describe() { # what the service said of a refusal, for an error the daemon logs: one line of printable characters, 200 at most, the token and the user key taken out should the service echo them. The delete names its range in octal because busybox tr reads [:print:] with -c as literal characters.
local d
d=$(jq -r '(.errors // []) | map(tostring) | join(", ")' <<<"$1" 2>/dev/null | tr -d '\000-\037\177') || d=""
d=${d//"$PUSHOVER_TOKEN"/}
d=${d//"$address"/}
printf '%s' "${d:0:200}"
}
case "$1" in
tell) # one message with its picture to the person, in the multipart form
address=$(arg address)
title=$(arg title)
text=$(arg text)
image=$(arg image)
[ -n "$address" ] || { echo "notify.image was handed no address" >&2; exit 1; }
[ -n "$image" ] || { echo "notify.image was handed no image" >&2; exit 1; }
: "${PUSHOVER_TOKEN:?PUSHOVER_TOKEN is not set}"
: "${YARDR_SOURCE_BASE:?YARDR_SOURCE_BASE is not set}"
[ -f "$image" ] || { echo "notify.image found no file at $image" >&2; exit 1; }
size=$(wc -c <"$image" | tr -d ' ')
if [ "$size" -gt 5242880 ]; then
echo "the picture is $size bytes, and Pushover takes 5242880 at most" >&2
exit 1
fi
# The words as the declared call has them: a title of 250 characters at
# most and a text of 1024. The API requires a message, so a picture sent
# with a title alone has the title for its message.
title=$(clip "$title" 250)
text=$(clip "$text" 1024)
if [ -z "$text" ]; then
text=$title
title=""
fi
[ -n "$text" ] || { echo "notify.image was handed neither a title nor a text" >&2; exit 1; }
# The part goes with a type and a name, which the API requires of an
# attachment. A picture the yard keeps is called by its hash, which names
# nothing a person could open: it goes as photo.png or photo.jpg.
mime=$(file -b --mime-type "$image" 2>/dev/null) || mime=""
[[ $mime =~ ^[a-z0-9.+_-]+/[a-z0-9.+_-]+$ ]] || mime=application/octet-stream
case "$mime" in
image/png) name=photo.png ;;
image/jpeg) name=photo.jpg ;;
image/gif) name=photo.gif ;;
image/webp) name=photo.webp ;;
image/bmp) name=photo.bmp ;;
*) name=photo ;;
esac
# The token and the user key go to curl as lines of a config on its
# stdin, written by the shell itself: an argument can be read by every
# process on the machine while curl runs. The words and the picture are
# data in arguments, never parsed for @ or <.
args=(--form-string "message=$text" -F "attachment=@$(qform "$image");type=$mime;filename=$name")
if [ -n "$title" ]; then args+=(--form-string "title=$title"); fi
out=$(printf 'url = %s\nform-string = %s\nform-string = %s\n' \
"$(cfgq "$YARDR_SOURCE_BASE/messages.json")" "$(cfgq "token=$PUSHOVER_TOKEN")" "$(cfgq "user=$address")" \
| curl -sS --fail-with-body -K - "${args[@]}") || {
code=$?
desc=$(describe "$out")
echo "the message with its picture failed${desc:+: $desc} (curl exit $code)" >&2
exit 1
}
jq -e '.status == 1' >/dev/null <<<"$out" 2>/dev/null || {
desc=$(describe "$out")
echo "the message with its picture was not taken${desc:+: $desc}" >&2
exit 1
}
printf '%s' '{"status":200}' ;;
*) echo "unknown: $1" >&2; exit 2 ;;
esac
name: pushover
about: A push to a person's phone through Pushover.
base: https://api.pushover.net/1
credential:
env: PUSHOVER_TOKEN # the application's token: the name, never the value
calls:
message:
kind: update
about: One push to one person
params:
user: { type: string, pattern: "^[A-Za-z0-9]{30}$" }
title: { type: string, max: 250 }
text: { type: string, max: 1024 }
link: { type: string, max: 512, optional: true }
request:
method: POST
path: /messages.json
body: { token: "{credential}", user: "{user}", title: "{title}", message: "{text}", url: "{link}" }
notify:
call: message
address: { param: user, env: PUSHOVER_USER } # the person's own key, read from the user's env_file
title: title # yardr's value -> the call's parameter
text: text
link: link
image: [pushover/notify, tell] # with a picture, the command sends it with the words instead
# Setting up Pushover The pack wires Pushover into the yard as the declared source `pushover`: one push to one person. What lands in a user's inbox then reaches their phone. Docs has the longer telling: `yardr docs pushover`. 1. **Two keys from Pushover.** On pushover.net, logged in, with its app on the phone: **Your User Key**, on the first page after login, 30 letters and digits — it is where *you* are reached; and **an application token**, "Create an Application/API Token" (pushover.net/apps/build), with any name, say `yardr` — it is the *yard's*. Pushover's free allowance is 10,000 messages a month for an account; yardr sends one user at most 20 an hour. 2. **Apply the pack.** As the operator, with the user told through it. The steps use `mckean` as the user; put the told person's name there. ```sh yardr pack apply pushover --notify mckean=pushover # source pushover is declared: 1 call under https://api.pushover.net/1 (yardr source show pushover) # user mckean: told outside the yard: through pushover, which is not set up: user mckean has no env_file: yardr user key mckean pushover gives PUSHOVER_USER ``` 3. **Give the yard the two keys.** No file is written by hand: each command asks for its key and shows nothing as it is typed or pasted, and the yard's server writes it to a file only you can read (`~/.yardr/sources/pushover.env`, `~/.yardr/users/mckean.env`). ```sh yardr source key pushover # PUSHOVER_TOKEN, the application token: the operator's to give yardr user key mckean pushover # PUSHOVER_USER, your user key: yours to give, or the operator's # user mckean: PUSHOVER_USER is set in …/mckean.env ``` Or have a crew member ask for them, and answer each on its card, in footplate or in `yardr chat` (`ctrl+o`), where the field shows dots: ```sh yardr bead ask <bead> "Pushover's application token" --secret pushover --context "pushover.net/apps/build" yardr bead ask <bead> "Your Pushover user key" --secret pushover --for-user mckean --context "The first page of pushover.net after login." ``` The keys go nowhere else: not into mail, chat, a bead or a pack. 4. **One test message.** ```sh yardr source check pushover --user mckean ``` The phone buzzes with "yardr: a test", and the command prints `pushover: told mckean: 200 in …ms`. This is one real message through Pushover, sent by the command itself. Anything but 200 fails the command with the status: 400 is a token or a user key it does not take, so give the two keys again (step 3). Mail to the user and their own timers are told from now on. `yardr user list` shows who is told through what; `yardr user set mckean notify=` ends it, and drops what was still waiting. A tell carries a picture too: the first image of a mail, or of the newest note on a bead that is done, goes with the words as the message's attachment (the command `pushover/notify` sends it, with curl, jq and file, which the yard's machine needs). Pushover takes a picture of 5,242,880 bytes at most and leaves it to the sender to make one fit, so a larger one is told as words alone, and the daemon's log says why: `user told without the picture`.