Python & Node clients

Python#

clients/python/herald.py is one file that uses only the Python standard library. Copy it next to your script, or put its folder on PYTHONPATH.

Minimal examplePython
from herald import Herald

Herald().notify("example.bidbot", "Bid accepted")

notify(app, title, **fields) returns the notification id. Every extra keyword becomes a field of the notification.

Realistic example

Register the app, send a notification with buttons, update it, and read History. The second notify reuses the id, so Herald replaces the banner instead of stacking a new one.

Python
from herald import Herald, HeraldError, HeraldUnavailable

h = Herald()
try:
    if h.is_available():
        h.register("example.bidbot", appName="BidBot",
                   callbackURL="http://127.0.0.1:5123/herald", defaults={"sound": "Glass"})
        h.notify("example.bidbot", "Counter-offer from Acme",
                 id="bid-42", body="They offer $3,900. Accept?", sound="Glass", snooze=True,
                 buttons=[{"label": "Accept", "callback": {"payload": {"decision": "accept"}}},
                          {"label": "Open", "url": "https://example.com/bids/42"}])
        h.notify("example.bidbot", "Counter-offer from Acme", id="bid-42",
                 body="They now offer $4,100. Accept?")
        for item in h.history("example.bidbot", limit=5):
            print(item["id"], item["notification"]["title"])
        h.dismiss("example.bidbot", "bid-42")
except HeraldUnavailable:
    print("Herald is not running")
except HeraldError as e:
    print("Herald refused the request:", e.status, e.message)

A callback button posts to the callbackURL you registered. A receiver needs only the standard library. Herald sends a JSON object and treats a 2xx answer as success. The fields of the object are in the callback request.

A receiver looks like this:

Python
import json
from http.server import BaseHTTPRequestHandler, HTTPServer

class Callback(BaseHTTPRequestHandler):
    def do_POST(self):
        event = json.loads(self.rfile.read(int(self.headers["Content-Length"])))
        print(event["notificationId"], event["action"], event.get("payload"))
        self.send_response(200)
        self.end_headers()

HTTPServer(("127.0.0.1", 5123), Callback).serve_forever()

Second instance

The module reads the default support folder. For another Herald, pass the port and token: Herald(port=48700, token=open("/tmp/herald-test/token").read().strip()).

Errors

Two exceptions matter:

  • HeraldUnavailable means Herald is not running. It is a subclass of HeraldError.
  • HeraldError carries status and message for an error answer.

The client waits 10 seconds by default. Herald(timeout=...) changes it.

Functions

FunctionWhat it doesEndpoint
is_available()Returns True when Herald answers.GET /v1/health
health()Returns ok, the version and the process id.GET /v1/health
notify(app, title, **fields)Shows a notification and returns its id.POST /v1/notify
register(app, **fields)Registers or updates an app.POST /v1/register
dismiss(app, id)Closes one banner.POST /v1/dismiss
dismiss_all(app)Closes every banner of an app.POST /v1/dismissAll
history(app, limit=50)Returns the stored notifications as a list.GET /v1/history
clear_history(app)Deletes an app's History.DELETE /v1/history
apps()Lists registered apps.GET /v1/apps

Node#

clients/node/herald.js is one file with no dependencies. It needs Node 18 or later, for the built-in fetch. It is a CommonJS module that an ES module can also import.

Minimal exampleJavaScript
const { Herald } = require('./herald');

await new Herald().notify('example.bidbot', 'Bid accepted');

notify(app, title, fields) returns the notification id. fields is an object of any other notification fields.

Realistic exampleJavaScript
const { Herald, HeraldError, HeraldUnavailable } = require('./herald');

const h = new Herald();
try {
  if (await h.isAvailable()) {
    await h.register('example.bidbot', {
      appName: 'BidBot', callbackURL: 'http://127.0.0.1:5123/herald', defaults: { sound: 'Glass' },
    });
    await h.notify('example.bidbot', 'Counter-offer from Acme', {
      id: 'bid-42', body: 'They offer $3,900. Accept?', sound: 'Glass', snooze: true,
      buttons: [
        { label: 'Accept', callback: { payload: { decision: 'accept' } } },
        { label: 'Open', url: 'https://example.com/bids/42' },
      ],
    });
    for (const item of await h.history('example.bidbot', 5)) console.log(item.id, item.notification.title);
    await h.dismiss('example.bidbot', 'bid-42');
  }
} catch (e) {
  if (e instanceof HeraldUnavailable) console.error('Herald is not running');
  else if (e instanceof HeraldError) console.error('Herald refused the request:', e.status, e.message);
  else throw e;
}

Second instance

The module reads the default support folder. For another Herald, pass the port and token: new Herald({ port: 48700, token: '...' }). The options are port, token and timeoutMs (default 10000).

Errors

Two errors matter:

  • HeraldUnavailable means Herald is not running. It extends HeraldError.
  • HeraldError carries status for an error answer.

isAvailable() returns false when Herald answers with an error, and throws HeraldUnavailable when it does not answer at all.

Functions

FunctionWhat it doesEndpoint
isAvailable()Resolves true when Herald answers.GET /v1/health
health()Resolves ok, the version and the process id.GET /v1/health
notify(app, title, fields)Shows a notification and resolves its id.POST /v1/notify
register(app, fields)Registers or updates an app.POST /v1/register
dismiss(app, id)Closes one banner.POST /v1/dismiss
dismissAll(app)Closes every banner of an app.POST /v1/dismissAll
history(app, limit = 50)Resolves the stored notifications as an array.GET /v1/history
clearHistory(app)Deletes an app's History.DELETE /v1/history
apps()Resolves the registered apps.GET /v1/apps

Registering a manifest#

A manifest declares the fields your app sends, the actions it offers and any assets, so the Designer can show them with sample values and a template can bind them. Register it once when your app starts. Saving a manifest for an app replaces the one before it. Then send the fields at the top level of each notification. The format is in the manifest reference; the route is PUT /v1/manifest.

The Python and Node clients have no manifest function, so the examples call the route with the client's own address and token. Swift has putManifest(_:).

Swift#

Swift
import HeraldClient

let manifest = try HeraldJSON.decoder().decode(HeraldManifest.self, from: Data("""
{"app": "example.bidbot", "appName": "BidBot", "version": 1,
 "fields": [{"key": "title", "type": "text", "required": true, "sample": "Bid accepted"},
            {"key": "amount", "type": "text", "sample": "$4,200"},
            {"key": "url", "type": "url"}],
 "actions": [{"id": "archive", "label": "Archive", "kind": "callback", "style": "destructive"}]}
""".utf8))
try await HeraldClient.shared.putManifest(manifest)

// Manifest fields go at the top level of the payload, the way an app sends them.
_ = try await HeraldClient.shared.notify(payload: .object([
    "app": .string("example.bidbot"), "title": .string("Bid accepted"),
    "amount": .string("$4,200"), "url": .string("https://example.com/bids/42"),
]))

Python#

Python
import json
import urllib.request
from herald import Herald

h = Herald()
manifest = {
    "app": "example.bidbot", "appName": "BidBot", "version": 1,
    "fields": [{"key": "title", "type": "text", "required": True, "sample": "Bid accepted"},
               {"key": "amount", "type": "text", "sample": "$4,200"},
               {"key": "url", "type": "url"}],
    "actions": [{"id": "archive", "label": "Archive", "kind": "callback", "style": "destructive"}],
}
request = urllib.request.Request(
    h.base_url + "/v1/manifest", method="PUT", data=json.dumps(manifest).encode(),
    headers={"Authorization": "Bearer " + h.token, "Content-Type": "application/json"})
urllib.request.urlopen(request).read()

h.notify("example.bidbot", "Bid accepted", amount="$4,200", url="https://example.com/bids/42")

Node#

JavaScript
const { Herald } = require('./herald');

const h = new Herald();
await fetch(h.baseUrl + '/v1/manifest', {
  method: 'PUT',
  headers: { Authorization: `Bearer ${h.token}`, 'Content-Type': 'application/json' },
  body: JSON.stringify({
    app: 'example.bidbot', appName: 'BidBot', version: 1,
    fields: [{ key: 'title', type: 'text', required: true, sample: 'Bid accepted' },
             { key: 'amount', type: 'text', sample: '$4,200' }],
    actions: [{ id: 'archive', label: 'Archive', kind: 'callback' }],
  }),
});
await h.notify('example.bidbot', 'Bid accepted', { amount: '$4,200' });

Shell#

shell
D="$HOME/Library/Application Support/Herald"
curl -s -X PUT "http://127.0.0.1:$(cat "$D/port")/v1/manifest" \
  -H "Authorization: Bearer $(cat "$D/token")" -H "Content-Type: application/json" \
  -d @manifest.json

The command line tool has no command that saves a manifest. It can delete one, with herald manifest delete. To read manifests back, use GET /v1/manifests.

Edit this page on GitHub

Esc
Getting started
Guides