commit - a9801d55e62952ed925510ec98eda85d55316cc8
commit + f0b13cc7907aa4956f3ddb6bc9d1d0582ba9f24a
blob - 713e36615cccf83123c9e9e350e9b59df47de6dd
blob + 6357a0b8e6fbfb2e36bb921a6ad493a5ffced8d8
--- README.md
+++ README.md
https://api.slack.com/reference/surfaces/formatting
+Block Kit
+---------
+If the message parses as JSON and is a non-empty array whose
+first element is an object with a string "type" field, sm posts
+it as a Block Kit "blocks" payload instead of plain text. A
+static fallback "text" is sent alongside the blocks so push and
+legacy notifications have something to display. Anything else,
+including messages that happen to be valid JSON but do not match
+that shape, is sent verbatim as text.
+
+ cat blocks.json | sm C03DEET2M18 -
+
+See the Block Kit reference:
+
+ https://api.slack.com/reference/block-kit/blocks
+
+
Configuration
-------------
sm reads a single environment variable:
blob - f796de77ad1b133e4e8f4c2773d8712cb784b8b8
blob + ead27c957c1e2973af69301537062a409b83c8fd
--- sm.1
+++ sm.1
See
.Lk https://api.slack.com/reference/surfaces/formatting
for the full reference.
+.Sh BLOCK KIT
+If
+.Ar message
+parses as JSON and is a non-empty array whose first element is an
+object with a string
+.Dq type
+field,
+.Nm
+posts it as a Block Kit
+.Dq blocks
+payload instead of plain text.
+A static fallback
+.Dq text
+is sent alongside the blocks so push and legacy notifications have
+something to display.
+Any other input, including messages that happen to be valid JSON
+but do not match that shape, is sent verbatim as text.
+See
+.Lk https://api.slack.com/reference/block-kit/blocks
+for the block reference.
.Sh ENVIRONMENT
.Bl -tag -width SLACK_USER_TOKEN
.It Ev SLACK_USER_TOKEN
.Bd -literal -offset indent
$ uname -a | sm C03DEET2M18 \-
.Ed
+.Pp
+Post a Block Kit payload from stdin:
+.Bd -literal -offset indent
+$ cat blocks.json | sm C03DEET2M18 \-
+.Ed
.Sh SEE ALSO
.Xr tls_init 3
.Sh AUTHORS
blob - daa5961a41a66a88aebb2ff37c335b3473a9e282
blob + 5af5cf5f9d004d463d7b4becf2bf5a64b68d2c81
--- sm.rs
+++ sm.rs
time::Duration,
};
-use jackson::{FromJson, ToJson, json_struct};
+use jackson::{FromJson, Value, json_struct};
json_struct! {
- struct PostMessage {
- channel: String,
- text: String,
- }
-}
-
-json_struct! {
struct PostResponse {
ok: bool,
error: Option<String>,
const PATH: &str = "/api/chat.postMessage";
const MAX_MSG_BYTES: usize = 40_000;
const TIMEOUT_SECS: u64 = 30;
+const BLOCKS_FALLBACK_TEXT: &str = "(message contains blocks)";
fn main() {
process::exit(run());
}
};
- let req = PostMessage {
- channel: channel.clone(),
- text: message,
- };
- let body = match req.to_json().stringify() {
+ let body = match build_body(channel, &message) {
Ok(s) => s,
Err(e) => {
eprintln!("sm: encode body: {e}");
}
//////////////////////////////////////////////////////////////////////////////
+// Payload
+//////////////////////////////////////////////////////////////////////////////
+
+// If the message parses as JSON and looks like a Block Kit payload — a
+// non-empty array whose first element is an object with a string "type"
+// field — send it as "blocks" with a static fallback "text" for
+// notifications. Otherwise the message is sent verbatim as "text".
+fn build_body(channel: &str, msg: &str) -> Result<String, String> {
+ let mut fields: Vec<(String, Value)> =
+ vec![("channel".to_string(), Value::String(channel.to_string()))];
+
+ match detect_blocks(msg) {
+ Some(blocks) => {
+ fields.push(("blocks".to_string(), blocks));
+ fields.push((
+ "text".to_string(),
+ Value::String(BLOCKS_FALLBACK_TEXT.to_string()),
+ ));
+ }
+ None => {
+ fields.push(("text".to_string(), Value::String(msg.to_string())));
+ }
+ }
+
+ Value::Object(fields).stringify().map_err(|e| e.to_string())
+}
+
+fn detect_blocks(msg: &str) -> Option<Value> {
+ let v: Value = msg.parse().ok()?;
+ let looks_like_blocks = v
+ .as_array()
+ .and_then(|a| a.first())
+ .and_then(Value::as_object)
+ .map(|o| {
+ o.iter()
+ .any(|(k, val)| k == "type" && val.as_str().is_some())
+ })
+ .unwrap_or(false);
+ if looks_like_blocks { Some(v) } else { None }
+}
+
+//////////////////////////////////////////////////////////////////////////////
// libtls FFI
//////////////////////////////////////////////////////////////////////////////
find(resp, b"\r\n\r\n").ok_or("no header terminator in response")?;
let body = std::str::from_utf8(&resp[sep + 4..])
.map_err(|e| format!("non-utf8 body: {e}"))?;
- let v: jackson::Value =
- body.parse().map_err(|e: jackson::Error| format!("json: {e}"))?;
- let r = PostResponse::from_json(&v)
- .map_err(|e| format!("decode body: {e}"))?;
+ let v: jackson::Value = body
+ .parse()
+ .map_err(|e: jackson::Error| format!("json: {e}"))?;
+ let r =
+ PostResponse::from_json(&v).map_err(|e| format!("decode body: {e}"))?;
if r.ok {
return Ok(());
}