Hilfe
Fertiger Code
Bausteine zum Kopieren: Client für Python und TypeScript, Webhook-Empfänger und Shell.
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.
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:
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 AppClient für TypeScript
Eine Datei samy.ts, läuft mit Node.js 18 oder neuer ohne weitere Pakete.
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.
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:
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"}'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:
./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