Services

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

#push#notifications#mobile#alerts

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 error

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.

ntfy_send(topic: str, message: str, options: map) → anyor an error

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 }.

Get it

$ quillo install ntfy               # ntfy.qwer into this folder$ quillo install ntfy --example     # and its example, ntfy.qwe

Or with curl:

$ curl -O https://quillolang.org/plugins/files/ntfy.qwer$ curl -O https://quillolang.org/plugins/files/ntfy.qwe

Then, in your program, all of it or only what you call:

main.qwe
use ntfy.qweruse ntfy_action, ntfy_send from ntfy      // Quillo 0.6: only these

It uses these crates, which cargo fetches the first time the plugin is built:

[dependencies]
ureq = "2"base64 = "0.22"serde_json = { version = "1", features = ["preserve_order"] }

Example

ntfy.qwe
// 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

ntfy.qwer
//! 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() } }))}
Firebase messaging 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.notificationspushmobilefirebase7 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.pushmobilehttp2jwt4 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.notificationspushpwabrowsers6 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.chatnotificationswebhooksdevops7 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.emailapinotificationsintegration7 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.emailnotificationssmtp6 views · 8 downloads
Thread
87Useful? Like it, or add to its thread below.

Thread 0 posts

On ntfy push: questions, tips, corrections, and code that uses it.

No posts yet. The first one opens the thread.