Discord
Messages 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.
Version 1.0.0 · 2 functions · 2 crates
Functions
What use discord.qwer gives a program, with the types Quillo sees. To take only some, or under other names: use discord_embed, discord_send from discord (Quillo 0.6).
discord_embed(spec: any) → anyor an errorAn embed from a map: title, description, url, color ("#5865f2" or a number), fields (a map of name to value, or [{ name, value, inline }]), image, thumbnail, author (a name, or { name, url, icon_url }), footer, timestamp (ISO 8601). Gives an error value naming the limit a part breaks (256 for a title, 4096 for a description, 25 fields...).
discord_send(webhook_url: str, message: any) → anyor an errorPosts to a channel's webhook (https://discord.com/api/webhooks/<id>/<token>; empty: DISCORD_WEBHOOK_URL). message holds content (up to 2000 characters) and/or embeds (up to 10, from discord_embed), and maybe username, avatar_url, thread_id (post in a thread) and mentions (true lets @everyone and user mentions ping; off by default). Gives { ok, status, id, retry_after }: when Discord says slow down, retry_after is the seconds to wait.
Get it
$ quillo install discord # discord.qwer into this folder$ quillo install discord --example # and its example, discord.qweOr with curl:
$ curl -O https://quillolang.org/plugins/files/discord.qwer$ curl -O https://quillolang.org/plugins/files/discord.qweThen, in your program, all of it or only what you call:
use discord.qweruse discord_embed, discord_send from discord // Quillo 0.6: only theseIt uses these crates, which cargo fetches the first time the plugin is built:
ureq = "2"serde_json = { version = "1", features = ["preserve_order"] }Example
// Deploys announced in the team's Discord channel, with an embed that is green when the deploy// passed and red when it failed.//// DISCORD_WEBHOOK_URL=https://discord.com/api/webhooks/... quillo run discord.qwe then open http://localhost:8080/deployeduse "discord.qwer"let WEBHOOK = env("DISCORD_WEBHOOK_URL", null) ?? `${env("SINK_URL", "")}/api/webhooks/1234/test-token`fn deploy_embed(version, passed) = discord_embed({ title: `Deployed ${version}`, url: `https://github.com/example/shop/releases/tag/${version}`, color: passed ? "#2e7d32" : "#c62828", fields: { Tests: passed ? "passed" : "failed", Region: "eu-west", Duration: "2m 41s" }, footer: "shop deploys", timestamp: "2026-10-29T09:00:00Z",})route GET /deployed = discord_send(WEBHOOK, { content: "@everyone a new version is out", username: "Deploy bot", embeds: [deploy_embed("v2.4.0", true)?] })?route GET /embeds = { passed: deploy_embed("v2.4.0", true)?, failed_color: deploy_embed("v2.4.1", false)?.color, too_long: discord_embed({ title: repeat("x", 300) }).error,}$ quillo run discord.qwe$ curl 'localhost:8080/deployed'$ curl 'localhost:8080/embeds'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
//! Messages to Discord channels through webhooks: content, embeds (title, color, fields, images, footer, timestamp) built and checked against Discord's limits, mentions kept off unless asked for, threads, and the rate limit's wait reported.//! version: 1.0.0//!//! [dependencies]//! ureq = "2"//! serde_json = { version = "1", features = ["preserve_order"] }use serde_json::{json, Map, Value};fn color(v: &Value) -> Result<Option<i64>, String> { match v { Value::Null => Ok(None), Value::Number(n) => Ok(n.as_i64()), Value::String(s) => { let h = s.trim().trim_start_matches('#'); let full: String = if h.len() == 3 { h.chars().flat_map(|c| [c, c]).collect() } else { h.to_string() }; i64::from_str_radix(&full, 16).map(Some).map_err(|_| format!("color `{}` is not #rrggbb", s)) } _ => Err("color is #rrggbb or a number".into()), }}/// An embed from a map: `title`, `description`, `url`, `color` ("#5865f2" or a number), `fields` (a map of name to value, or/// `[{ name, value, inline }]`), `image`, `thumbnail`, `author` (a name, or `{ name, url, icon_url }`), `footer`, `timestamp`/// (ISO 8601). Gives an error value naming the limit a part breaks (256 for a title, 4096 for a description, 25 fields...).pub fn discord_embed(spec: serde_json::Value) -> Result<Value, String> { let s = |k: &str| spec[k].as_str(); let mut e = Map::new(); let limit = |k: &str, v: &str, n: usize| if v.chars().count() > n { Err(format!("an embed's {} is at most {} characters ({} given)", k, n, v.chars().count())) } else { Ok(()) }; if let Some(t) = s("title") { limit("title", t, 256)?; e.insert("title".into(), json!(t)); } if let Some(d) = s("description") { limit("description", d, 4096)?; e.insert("description".into(), json!(d)); } if let Some(u) = s("url") { e.insert("url".into(), json!(u)); } if let Some(c) = color(&spec["color"])? { e.insert("color".into(), json!(c)); } let fields: Vec<Value> = match &spec["fields"] { Value::Object(m) => m.iter().map(|(k, v)| json!({ "name": k, "value": v.as_str().map(str::to_string).unwrap_or_else(|| v.to_string()), "inline": true })).collect(), Value::Array(a) => a.clone(), _ => vec![], }; if fields.len() > 25 { return Err(format!("an embed has at most 25 fields ({} given)", fields.len())); } for f in &fields { limit("field name", f["name"].as_str().unwrap_or(""), 256)?; limit("field value", f["value"].as_str().unwrap_or(""), 1024)?; } if !fields.is_empty() { e.insert("fields".into(), json!(fields)); } if let Some(i) = s("image") { e.insert("image".into(), json!({ "url": i })); } if let Some(i) = s("thumbnail") { e.insert("thumbnail".into(), json!({ "url": i })); } match &spec["author"] { Value::String(a) => { e.insert("author".into(), json!({ "name": a })); } Value::Object(a) => { e.insert("author".into(), Value::Object(a.clone())); } _ => {} } match &spec["footer"] { Value::String(f) => { limit("footer", f, 2048)?; e.insert("footer".into(), json!({ "text": f })); } Value::Object(f) => { e.insert("footer".into(), Value::Object(f.clone())); } _ => {} } if let Some(t) = s("timestamp") { e.insert("timestamp".into(), json!(t)); } let total: usize = ["title", "description"].iter().filter_map(|k| e.get(*k).and_then(|v| v.as_str())).map(|v| v.chars().count()).sum::<usize>() + fields.iter().map(|f| f["name"].as_str().unwrap_or("").chars().count() + f["value"].as_str().unwrap_or("").chars().count()).sum::<usize>(); if total > 6000 { return Err(format!("an embed's text is at most 6000 characters in all ({} given)", total)); } Ok(Value::Object(e))}/// Posts to a channel's webhook (`https://discord.com/api/webhooks/<id>/<token>`; empty: DISCORD_WEBHOOK_URL). `message` holds/// `content` (up to 2000 characters) and/or `embeds` (up to 10, from `discord_embed`), and maybe `username`, `avatar_url`,/// `thread_id` (post in a thread) and `mentions` (true lets @everyone and user mentions ping; off by default). Gives/// `{ ok, status, id, retry_after }`: when Discord says slow down, `retry_after` is the seconds to wait.pub fn discord_send(webhook_url: &str, message: serde_json::Value) -> Result<Value, String> { let url = if webhook_url.is_empty() { std::env::var("DISCORD_WEBHOOK_URL").map_err(|_| "no webhook URL, and DISCORD_WEBHOOK_URL is not set".to_string())? } else { webhook_url.to_string() }; let m = message.as_object().ok_or("the message is a map: { content, embeds }")?; let content = m.get("content").and_then(|v| v.as_str()).unwrap_or(""); let embeds = m.get("embeds").and_then(|v| v.as_array()).cloned().unwrap_or_default(); if content.is_empty() && embeds.is_empty() { return Err("a message needs content or embeds".into()); } if content.chars().count() > 2000 { return Err(format!("content is at most 2000 characters ({} given)", content.chars().count())); } if embeds.len() > 10 { return Err("a message has at most 10 embeds".into()); } let mut body = Map::new(); if !content.is_empty() { body.insert("content".into(), json!(content)); } if !embeds.is_empty() { body.insert("embeds".into(), json!(embeds)); } for k in ["username", "avatar_url", "tts"] { if let Some(v) = m.get(k) { body.insert(k.into(), v.clone()); } } let mentions = m.get("mentions").and_then(|v| v.as_bool()).unwrap_or(false); body.insert("allowed_mentions".into(), if mentions { json!({ "parse": ["users", "roles", "everyone"] }) } else { json!({ "parse": [] }) }); let mut full = format!("{}{}wait=true", url, if url.contains('?') { "&" } else { "?" }); if let Some(t) = m.get("thread_id").and_then(|v| v.as_str()) { full.push_str(&format!("&thread_id={}", t)); } let res = ureq::post(&full).timeout(std::time::Duration::from_secs(15)).set("Content-Type", "application/json").send_string(&Value::Object(body).to_string()); let (status, text) = match res { 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(e.to_string()), }; 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"], "retry_after": if status == 429 { v["retry_after"].clone() } else { Value::Null }, "error": if ok { Value::Null } else { v["message"].clone() }, }))}Related
teams.qwer · 1.0.0Post to Microsoft Teams channels through incoming webhooks: Adaptive Cards built from headings, facts, columns, images and link buttons and checked before they go, sent to Workflows or classic connector URLs, with the failures Teams reports as a 200 read as errors and 429s waited out.3 views · 6 downloadsSlack slack.qwer · 1.0.0Messages to Slack through an incoming webhook or chat.postMessage and chat.update, with Block Kit built from a short list (headers, text, fields, buttons, context), mrkdwn escaped and Slack's limits checked.7 views · 8 downloadsTelegram bots telegram.qwer · 1.0.0A Telegram bot through the Bot API: messages split at 4096 characters, photos by URL or upload, inline keyboards checked before they are sent, updates by webhook or long polling read into one shape, the webhook's secret token compared, and the Login Widget's hash verified.4 views · 8 downloadsWhatsApp Cloud API whatsapp.qwer · 1.0.0Send WhatsApp text and template messages through Meta's Cloud API, with the 24-hour window and missing templates named when they refuse a message, answer the webhook's verify handshake, check X-Hub-Signature-256, and flatten webhook bodies into the messages and statuses they carry.2 views · 6 downloadsAsana asana.qwer · 1.0.0Asana's API without an SDK: tasks, projects and comments in Asana's data envelope, offset pages walked with an expired one named, opt_fields, Retry-After waited out, and webhooks from their handshake on: X-Hook-Secret answered and kept, refused once one is held, and each delivery's X-Hook-Signature checked.3 views · 6 downloadsCalendly calendly.qwer · 1.0.0Calendly's API v2 without an SDK: event types, scheduled events and their invitees through page tokens, cancels, webhook subscriptions with a signing key insisted on, error details read, resource addresses shortened to uuids, and the Calendly-Webhook-Signature checked with its timestamp tolerance.3 views · 9 downloadsThread 0 posts
On Discord: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.