Hilfe

Fertiger Code

Bausteine zum Kopieren: Client für Python und TypeScript, Webhook-Empfänger und Shell.

Als Markdown öffnen

Bausteine zum Kopieren. Alle lesen den Schlüssel aus der Umgebungsvariable SAMY_KEY.

Client für Python

Eine Datei samy.py mit zwei Funktionen: Nachricht schicken und die Seite einer App setzen.

Python
import os, requests  # pip install requests

API = "https://api.packlets.app"
HEADERS = {"Authorization": f"Bearer {os.environ['SAMY_KEY']}"}

def _call(method, path, body):
    res = requests.request(method, API + path, headers=HEADERS, json=body, timeout=10)
    if not res.ok:
        raise RuntimeError(f"Samy {res.status_code}: {res.text}")
    return res.json()

def push(text, title=None, url=None, image=None, buttons=None, elements=None, priority="normal"):
    """Nachricht schicken. Gibt die message_id zurück."""
    body = {"text": text, "title": title, "url": url, "image": image,
            "buttons": buttons or [], "elements": elements or [], "priority": priority}
    return _call("POST", "/v1/push", body)["message_id"]

def set_page(page):
    """App: die Seite setzen, die die App zeigt (https-Adresse)."""
    return _call("PUT", "/v1/ui", {"page": page})

So nutzt du sie:

Python
from samy import push, set_page

push("Backup von heute Nacht ist fertig.", title="Backup", priority="silent")

push("shop-api antwortet seit 9:38 Uhr nicht.", title="Server down", priority="high",
     buttons=[{"id": "restart", "label": "Neu starten"}])

push("23 Bestellungen, 1.204 € Umsatz.", title="Tagesbericht", elements=[
    {"type": "chart", "id": "umsatz", "label": "Umsatz der Woche", "unit": "€",
     "points": [{"label": "Mo", "value": 860}, {"label": "Di", "value": 1204}]},
])

set_page("https://dein-server.de/rechnungen")  # nur für ein Packlet der Art App

Client für TypeScript

Eine Datei samy.ts, läuft mit Node.js 18 oder neuer ohne weitere Pakete.

TypeScript
const API = "https://api.packlets.app";

async function call<T>(method: string, path: string, body: unknown): Promise<T> {
  const res = await fetch(API + path, {
    method,
    headers: { Authorization: `Bearer ${process.env.SAMY_KEY}`, "Content-Type": "application/json" },
    body: JSON.stringify(body),
  });
  if (!res.ok) throw new Error(`Samy ${res.status}: ${await res.text()}`);
  return (await res.json()) as T;
}

/** Nachricht schicken. Gibt die message_id zurück. */
export async function push(message: SamyMessage): Promise<string> {
  return (await call<{ message_id: string }>("POST", "/v1/push", message)).message_id;
}

/** App: die Seite setzen, die die App zeigt (https-Adresse) */
export const setPage = (page: string) => call<{ ok: true; page: string }>("PUT", "/v1/ui", { page });

Typen für TypeScript

Gehören in dieselbe Datei. Damit prüft der Editor schon beim Schreiben, ob Nachrichten, Elemente und Ereignisse stimmen.

TypeScript
export type SamyMessage = {
  text: string;
  title?: string;
  url?: string;
  image?: string;
  buttons?: { id: string; label: string; url?: string }[];
  elements?: SamyElement[];
  priority?: "normal" | "high" | "silent";
};

type Layout = {
  /** Pro Nachricht eindeutig: Buchstaben, Ziffern, _ und -, 1 bis 40 Zeichen */
  id: string;
  /** Spalten im Raster, Standard 6 (ganze Breite) */
  width?: 1 | 2 | 3 | 4 | 5 | 6;
  /** Mindesthöhe in Punkten */
  height?: number;
};

export type SamyElement =
  | (Omit<Layout, "height"> & { type: "text"; text: string; style?: "title" | "body" | "caption" })
  | (Layout & { type: "button"; label: string; style?: "primary" | "secondary" })
  | (Layout & { type: "toggle"; label: string; description?: string; value?: boolean })
  | (Layout & { type: "input"; label: string; placeholder?: string; button?: string; value?: string })
  | (Layout & {
      type: "chart";
      label: string;
      points?: { label: string; value: number }[];
      chart?: "bar" | "line";
      unit?: string;
      highlight?: number;
    })
  | (Layout & { type: "table"; columns: string[]; rows?: (string | number)[][]; label?: string });

/** Ereignisse am Webhook */
export type SamyEvent =
  | { type: "button"; packlet_id: string; message_id: string; button_id: string; pressed_at: string }
  | { type: "message"; packlet_id: string; message_id: string; text: string; sent_at: string }
  | {
      type: "ui";
      packlet_id: string;
      /** Die Nachricht, zu der das Element gehört */
      message_id: string;
      element_id: string;
      /** Schalter: true/false, Eingabefeld: Text, Button: null */
      value: boolean | string | null;
      changed_at: string;
    };

Webhook-Empfänger

Prüft die Signatur und verteilt die drei Ereignisse. Die Erklärung dazu steht unter Webhook einrichten.

import hashlib, hmac, os
from flask import Flask, request  # pip install flask

app = Flask(__name__)
SECRET = hashlib.sha256(os.environ["SAMY_KEY"].encode()).hexdigest().encode()

@app.post("/samy")
def samy_webhook():
    expected = "sha256=" + hmac.new(SECRET, request.get_data(), hashlib.sha256).hexdigest()
    if not hmac.compare_digest(expected, request.headers.get("X-Samy-Signature", "")):
        return "", 401

    event = request.get_json()
    if event["type"] == "button":
        print("Knopf", event["button_id"], "in Nachricht", event["message_id"])
    elif event["type"] == "ui":
        print("Element", event["element_id"], "in Nachricht", event["message_id"], "ist jetzt", event["value"])
    elif event["type"] == "message":
        print("Chat:", event["text"])
    return "", 204

app.run(port=8080)

Shell

Für Cronjobs und Skripte. Die erste Fassung schickt JSON, die zweite ist die kürzeste, die geht:

Shell
curl -fsS https://api.packlets.app/v1/push \
  -H "Authorization: Bearer $SAMY_KEY" \
  -H "Content-Type: application/json" \
  -d '{"title":"Backup","text":"Backup von heute Nacht ist fertig.","priority":"silent"}'
Shell
curl -fsS https://api.packlets.app/v1/push -H "Authorization: Bearer $SAMY_KEY" -d text="Backup fertig"

Mit -f endet curl bei einem Fehler mit einem Status ungleich null. So merkt dein Skript, wenn etwas nicht geklappt hat.

Eine Meldung nur im Fehlerfall:

Shell
./backup.sh || curl -fsS https://api.packlets.app/v1/push \
  -H "Authorization: Bearer $SAMY_KEY" \
  -d title="Backup" -d text="Das Backup ist fehlgeschlagen." -d priority=high