ntfy push
Push notifications to phones and desktops through ntfy.sh or your own ntfy server: title, priority, tags and emoji, a link, action buttons, attachments, Markdown and delayed delivery, with a token or a password.
Version 1.0.0 · 2 functions · 3 crates
Functions
What use ntfy.qwer gives a program, with the types Quillo sees. To take only some, or under other names: use ntfy_action, ntfy_send from ntfy (Quillo 0.6).
ntfy_action(kind: str, label: str, url: str, clear: bool, options: map) → anyor an errorAn action button for ntfy_send's actions: kind "view" (opens url), "http" (the phone calls url; options may hold method, headers, body) or "broadcast" (an Android intent). clear closes the notification after a tap.
ntfy_send(topic: str, message: str, options: map) → anyor an errorPublishes message to topic. options may hold title, priority (1 min to 5 max, or "min", "low", "default", "high", "urgent"), tags (["warning", "skull"]: emoji names become emoji), click (a URL to open), actions (up to 3, from ntfy_action), attach (a file's URL) and filename, icon, markdown (true), delay ("30m", "tomorrow, 10am", a Unix time), email (also sent there). The server is NTFY_URL (else https://ntfy.sh) or options.server; NTFY_TOKEN or options.token signs in with an access token, NTFY_USER and NTFY_PASSWORD with a password. Gives { ok, status, id, time, error }.
Get it
$ quillo install ntfy # ntfy.qwer into this folder$ quillo install ntfy --example # and its example, ntfy.qweOr with curl:
$ curl -O https://quillolang.org/plugins/files/ntfy.qwer$ curl -O https://quillolang.org/plugins/files/ntfy.qweThen, in your program, all of it or only what you call:
use ntfy.qweruse ntfy_action, ntfy_send from ntfy // Quillo 0.6: only theseIt uses these crates, which cargo fetches the first time the plugin is built:
ureq = "2"base64 = "0.22"serde_json = { version = "1", features = ["preserve_order"] }Example
// The shop's owner gets a push on their phone for every big order, with buttons to open it or// mark it packed, through ntfy (the free ntfy.sh, or a server of their own).//// NTFY_URL=https://ntfy.sh NTFY_TOPIC=shop-orders-x7k2 quillo run ntfy.qwe then open http://localhost:8080/big-order// (subscribe to the topic in the ntfy app first)use "ntfy.qwer"let TOPIC = env("NTFY_TOPIC", "shop-orders-demo")let SERVER = env("NTFY_URL", null) ?? env("SINK_URL", null)route GET /big-order { let order = { id: "A-1042", total: "€420.00", customer: "Ada" } ntfy_send(TOPIC, `**${order.customer}** ordered for ${order.total}.`, { server: SERVER, title: `Big order ${order.id}`, priority: "high", tags: ["moneybag", "shop"], click: `https://shop.example.com/admin/orders/${order.id}`, markdown: true, actions: [ ntfy_action("view", "Open", `https://shop.example.com/admin/orders/${order.id}`, false, {})?, ntfy_action("http", "Mark packed", `https://shop.example.com/api/orders/${order.id}/packed`, true, { method: "POST", headers: { authorization: "Bearer shop-token" } })?, ], })?}route GET /actions = { view: ntfy_action("view", "Open", "https://shop.example.com", true, {})?, bad_kind: ntfy_action("open", "x", "https://x", false, {}).error, bad_topic: ntfy_send("orders and more", "hi", {}).error, bad_priority: ntfy_send(TOPIC, "hi", { server: SERVER, priority: 9 }).error,}$ quillo run ntfy.qwe$ curl 'localhost:8080/big-order'$ curl 'localhost:8080/actions'It talks to the Redis that REDIS_URL names, else Redis on this machine. The site's test runs it against a stand-in.
Source
//! Push notifications to phones and desktops through ntfy (ntfy.sh or your own server): a topic, title, priority, tags and emoji, a link to open, action buttons, attachments, Markdown and delayed delivery, with an access token or a password.//! version: 1.0.0//!//! [dependencies]//! ureq = "2"//! base64 = "0.22"//! serde_json = { version = "1", features = ["preserve_order"] }use base64::engine::general_purpose::STANDARD;use base64::Engine;use serde_json::{json, Map, Value};fn env(k: &str) -> Option<String> { std::env::var(k).ok().filter(|v| !v.trim().is_empty())}/// An action button for `ntfy_send`'s `actions`: `kind` "view" (opens `url`), "http" (the phone calls `url`; `options` may hold/// `method`, `headers`, `body`) or "broadcast" (an Android intent). `clear` closes the notification after a tap.pub fn ntfy_action(kind: &str, label: &str, url: &str, clear: bool, options: std::collections::HashMap<String, serde_json::Value>) -> Result<Value, String> { if !["view", "http", "broadcast"].contains(&kind) { return Err(format!("action `{}` is view, http or broadcast", kind)); } if label.is_empty() { return Err("an action needs a label".into()); } let mut a = Map::new(); a.insert("action".into(), json!(kind)); a.insert("label".into(), json!(label)); if kind != "broadcast" { if !(url.starts_with("https://") || url.starts_with("http://") || kind == "view") { return Err(format!("`{}` is not a URL", url)); } a.insert("url".into(), json!(url)); } if clear { a.insert("clear".into(), json!(true)); } for (k, v) in options { a.insert(k, v); } Ok(Value::Object(a))}/// Publishes `message` to `topic`. `options` may hold `title`, `priority` (1 min to 5 max, or "min", "low", "default", "high",/// "urgent"), `tags` (`["warning", "skull"]`: emoji names become emoji), `click` (a URL to open), `actions` (up to 3, from/// `ntfy_action`), `attach` (a file's URL) and `filename`, `icon`, `markdown` (true), `delay` ("30m", "tomorrow, 10am", a Unix/// time), `email` (also sent there). The server is NTFY_URL (else https://ntfy.sh) or `options.server`; NTFY_TOKEN or `options.token`/// signs in with an access token, NTFY_USER and NTFY_PASSWORD with a password. Gives `{ ok, status, id, time, error }`.pub fn ntfy_send(topic: &str, message: &str, options: std::collections::HashMap<String, serde_json::Value>) -> Result<Value, String> { if topic.is_empty() || topic.len() > 64 || !topic.chars().all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_') { return Err(format!("`{}` is not a topic name (letters, digits, - and _, up to 64)", topic)); } let o = |k: &str| options.get(k).filter(|v| !v.is_null()); let s = |k: &str| o(k).and_then(|v| v.as_str()).map(str::to_string).filter(|v| !v.is_empty()); let server = s("server").or_else(|| env("NTFY_URL")).unwrap_or_else(|| "https://ntfy.sh".into()); let mut body = Map::new(); body.insert("topic".into(), json!(topic)); body.insert("message".into(), json!(message)); if let Some(p) = o("priority") { let n = match p { Value::Number(n) => n.as_i64().unwrap_or(3), Value::String(w) => match w.as_str() { "min" => 1, "low" => 2, "default" => 3, "high" => 4, "urgent" | "max" => 5, other => other.parse().map_err(|_| format!("priority `{}` is 1 to 5, or min, low, default, high, urgent", other))? }, _ => 3, }; if !(1..=5).contains(&n) { return Err(format!("priority {} is 1 to 5", n)); } body.insert("priority".into(), json!(n)); } if let Some(t) = o("tags") { let tags: Vec<String> = match t { Value::String(s) => s.split(',').map(|x| x.trim().to_string()).collect(), Value::Array(a) => a.iter().filter_map(|x| x.as_str().map(str::to_string)).collect(), _ => vec![] }; body.insert("tags".into(), json!(tags)); } if let Some(a) = o("actions").and_then(|v| v.as_array()) { if a.len() > 3 { return Err("ntfy shows at most 3 actions".into()); } body.insert("actions".into(), json!(a)); } for k in ["title", "click", "attach", "filename", "icon", "email", "call"] { if let Some(v) = s(k) { body.insert(k.into(), json!(v)); } } if let Some(d) = o("delay") { body.insert("delay".into(), json!(d.as_str().map(str::to_string).unwrap_or_else(|| d.to_string()))); } if o("markdown").and_then(|v| v.as_bool()) == Some(true) { body.insert("markdown".into(), json!(true)); } let mut req = ureq::post(server.trim_end_matches('/')).timeout(std::time::Duration::from_secs(15)).set("Content-Type", "application/json"); if let Some(t) = s("token").or_else(|| env("NTFY_TOKEN")) { req = req.set("Authorization", &format!("Bearer {}", t)); } else if let (Some(u), Some(p)) = (env("NTFY_USER"), env("NTFY_PASSWORD")) { req = req.set("Authorization", &format!("Basic {}", STANDARD.encode(format!("{}:{}", u, p)))); } let (status, text) = match req.send_string(&Value::Object(body).to_string()) { Ok(r) => (r.status(), r.into_string().unwrap_or_default()), Err(ureq::Error::Status(c, r)) => (c, r.into_string().unwrap_or_default()), Err(e) => return Err(format!("ntfy: {}", e)), }; let v: Value = serde_json::from_str(&text).unwrap_or(Value::Null); let ok = (200..300).contains(&status); Ok(json!({ "ok": ok, "status": status, "id": v["id"], "time": v["time"], "error": if ok { Value::Null } else { v["error"].clone() } }))}Related
fcm.qwer · 1.0.0Notifications to Android, iOS and web apps through Firebase Cloud Messaging v1, with the service account's RS256 token made and kept until it expires.7 views · 8 downloadsApple push notifications apns.qwer · 1.0.0Send push notifications to iPhones through APNs over HTTP/2: alert and background payloads built and sized, an ES256 provider token made and renewed, collapse ids and push types set, many devices on one kept connection, and Apple's reasons such as BadDeviceToken and Unregistered read back so dead tokens can be dropped.4 views · 6 downloadsWeb push web_push.qwer · 1.0.0Browser push notifications with no third party: VAPID keys, and messages encrypted for each subscription and sent to its push service.6 views · 7 downloadsDiscord discord.qwer · 1.0.0Messages to Discord channels through webhooks: embeds with color, fields, images and timestamps built and checked against Discord's limits, mentions off unless asked for, threads, and the rate limit's wait reported.7 views · 8 downloadsEmail APIs mail_api.qwer · 1.0.0Email sent through Resend, Postmark, SendGrid or Mailgun's HTTP APIs with one call and one message shape, attachments included, the provider picked by a word; and the request shown before it is sent.7 views · 7 downloadsEmail over SMTP smtp.qwer · 1.0.0Email through any SMTP provider (Postmark, SES, Mailgun, Gmail): text, HTML or both, to one or several people.6 views · 8 downloadsThread 0 posts
On ntfy push: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.