///|
/// Platform presets: every difference between the supported notification
/// platforms lives in the `match` below, and each arm describes only the
/// payload shape, where the credential goes and how the request is signed.
/// Adding a platform means adding one arm plus one row in the README table —
/// nothing else in the library knows that platforms exist.
///|
fn default_format(preset : String) -> String {
match preset {
"slack" | "discord" | "feishu" | "dingtalk" | "wecom" => "markdown"
_ => "text"
}
}
///|
/// Render one message for one target: endpoint (any signature already folded
/// in), headers, body, and — for email — the whole RFC5322 message the
/// transport uploads.
fn render_delivery(
target : Target,
msg : Message,
ctx : Context,
) -> Delivery raise NotifyError {
let format = target.format.unwrap_or(default_format(target.preset))
match target.preset {
"feishu" => render_feishu(target, msg, ctx, format)
"dingtalk" => render_dingtalk(target, msg, ctx, format)
"wecom" => render_wecom(target, msg, ctx, format)
"slack" => render_slack(target, msg, ctx, format)
"discord" => render_discord(target, msg, ctx, format)
"webhook" => render_generic(target, msg, ctx, format)
"email" => render_email(target, msg, ctx, format)
_ => raise NotifyError::InvalidPreset(target.preset)
}
}
///|
fn json_delivery(
url : String,
body : String,
headers : Array[(String, String)],
) -> Delivery {
{
url,
http_method: "POST",
content_type: Some("application/json; charset=utf-8"),
headers,
body,
mail: None,
}
}
///|
fn render_feishu(
target : Target,
msg : Message,
ctx : Context,
format : String,
) -> Delivery {
let fields : Array[(String, String)] = []
if format == "markdown" || format == "card" {
fields.push(("msg_type", @fndash.jstr("interactive")))
fields.push(
(
"card",
@fndash.jobj([
(
"elements",
@fndash.jarr([
@fndash.jobj([
("tag", @fndash.jstr("div")),
(
"text",
@fndash.jobj([
("tag", @fndash.jstr("lark_md")),
("content", @fndash.jstr(markdown_body(msg, ctx))),
]),
),
]),
]),
),
]),
),
)
} else {
fields.push(("msg_type", @fndash.jstr("text")))
fields.push(
("content", @fndash.jobj([("text", @fndash.jstr(plain_body(msg, ctx)))])),
)
}
match target.secret {
Some(secret) =>
if !secret.is_empty() {
let seconds = msg.ts_ms / 1000UL
fields.push(("timestamp", @fndash.jstr(seconds.to_string())))
fields.push(("sign", @fndash.jstr(feishu_signature(secret, seconds))))
}
None => ()
}
json_delivery(target.url, @fndash.jobj(fields), [])
}
///|
fn render_dingtalk(
target : Target,
msg : Message,
ctx : Context,
format : String,
) -> Delivery {
let body = if format == "markdown" || format == "card" {
@fndash.jobj([
("msgtype", @fndash.jstr("markdown")),
(
"markdown",
@fndash.jobj([
("title", @fndash.jstr(msg.title)),
("text", @fndash.jstr(markdown_body(msg, ctx))),
]),
),
])
} else {
@fndash.jobj([
("msgtype", @fndash.jstr("text")),
("text", @fndash.jobj([("content", @fndash.jstr(plain_body(msg, ctx)))])),
])
}
let mut url = target.url
match target.secret {
Some(secret) =>
if !secret.is_empty() {
url = url +
(if url.contains("?") { "&" } else { "?" }) +
"timestamp=" +
msg.ts_ms.to_string() +
"&sign=" +
dingtalk_signature(secret, msg.ts_ms)
}
None => ()
}
json_delivery(url, body, [])
}
///|
fn render_wecom(
target : Target,
msg : Message,
ctx : Context,
format : String,
) -> Delivery {
let body = if format == "markdown" || format == "card" {
@fndash.jobj([
("msgtype", @fndash.jstr("markdown")),
(
"markdown",
@fndash.jobj([("content", @fndash.jstr(markdown_body(msg, ctx)))]),
),
])
} else {
@fndash.jobj([
("msgtype", @fndash.jstr("text")),
("text", @fndash.jobj([("content", @fndash.jstr(plain_body(msg, ctx)))])),
])
}
json_delivery(target.url, body, [])
}
///|
fn render_slack(
target : Target,
msg : Message,
ctx : Context,
format : String,
) -> Delivery {
let body = if format == "card" || format == "markdown" {
@fndash.jobj([
("text", @fndash.jstr(msg.title)),
(
"blocks",
@fndash.jarr([
@fndash.jobj([
("type", @fndash.jstr("section")),
(
"text",
@fndash.jobj([
("type", @fndash.jstr("mrkdwn")),
("text", @fndash.jstr(markdown_body(msg, ctx))),
]),
),
]),
]),
),
])
} else {
@fndash.jobj([("text", @fndash.jstr(markdown_body(msg, ctx)))])
}
json_delivery(target.url, body, [])
}
///|
fn render_discord(
target : Target,
msg : Message,
ctx : Context,
format : String,
) -> Delivery {
let body = if format == "card" || format == "markdown" {
@fndash.jobj([
("content", @fndash.jstr(msg.title)),
(
"embeds",
@fndash.jarr([
@fndash.jobj([
("title", @fndash.jstr(msg.title)),
("description", @fndash.jstr(markdown_body(msg, ctx))),
]),
]),
),
])
} else {
@fndash.jobj([("content", @fndash.jstr(markdown_body(msg, ctx)))])
}
json_delivery(target.url, body, [])
}
///|
/// The generic preset: one stable, documented JSON shape for everything the host application
/// does not ship a preset for (self-hosted gateways, other webhook platforms).
fn render_generic(
target : Target,
msg : Message,
ctx : Context,
format : String,
) -> Delivery {
ignore(format)
let body = @fndash.jobj([
("event", @fndash.jstr(msg.event)),
("level", @fndash.jstr(msg.level)),
("title", @fndash.jstr(msg.title)),
("body", @fndash.jstr(msg.body)),
("host", @fndash.jstr(ctx.host)),
("timestamp", @fndash.jstr(msg.ts_ms.to_string())),
("fields", @fndash.jobj(json_pairs(msg.fields))),
])
let headers = []
match target.secret {
Some(secret) =>
if !secret.is_empty() {
headers.push(("Authorization", "Bearer " + secret))
}
None => ()
}
json_delivery(target.url, body, headers)
}
///|
fn json_pairs(fields : Array[(String, String)]) -> Array[(String, String)] {
fields.map(fn(pair) {
let (key, value) = pair
(key, @fndash.jstr(value))
})
}
///|
/// Email is the one preset whose transport is SMTP, so it renders a whole
/// message (headers plus UTF-8 body) for the transport to send.
fn render_email(
target : Target,
msg : Message,
ctx : Context,
format : String,
) -> Delivery {
ignore(format)
let buf = StringBuilder()
buf.write_string("From: " + target.from.unwrap_or("") + "\n")
buf.write_string("To: " + target.to.unwrap_or([]).join(", ") + "\n")
buf.write_string(
"Subject: =?UTF-8?B?" + @base64.encode(@utf8.encode(msg.title)[:]) + "?=\n",
)
buf.write_string("Date: " + @dateku.rfc5322(msg.ts_ms) + "\n")
buf.write_string("MIME-Version: 1.0\n")
buf.write_string("Content-Type: text/plain; charset=UTF-8\n")
buf.write_string("Content-Transfer-Encoding: 8bit\n")
buf.write_string("X-Host: " + ctx.host + "\n")
buf.write_string("\n")
buf.write_string(markdown_body(msg, ctx))
buf.write_string("\n")
{
url: target.url,
http_method: "POST",
content_type: None,
headers: [],
body: "",
mail: Some(buf.to_string()),
}
}