Compare commits

..

View file

@ -105,18 +105,6 @@ async fn fetch_json(
resp.json().await.ok() resp.json().await.ok()
} }
/// Map a Forgejo notification `subject.type` to a human-readable label.
/// Known values: "Pull", "Issue", "Commit", "Repository". Any unknown
/// type is passed through as-is so new Forgejo types degrade gracefully
/// rather than silently collapsing into a generic label.
fn notif_type_label(t: &str) -> &str {
match t {
"Pull" => "PR",
"Issue" => "issue",
other => other,
}
}
/// Truncate a string to `max` bytes at a char boundary, appending `…` if cut. /// Truncate a string to `max` bytes at a char boundary, appending `…` if cut.
fn truncate(s: &str, max: usize) -> String { fn truncate(s: &str, max: usize) -> String {
if s.len() <= max { if s.len() <= max {
@ -170,7 +158,11 @@ async fn format_notification(
.unwrap_or("") .unwrap_or("")
.trim(); .trim();
let kind = format!("comment on {}", notif_type_label(notif_type)); let kind = match notif_type {
"Pull Request" => "comment on PR",
"Issue" => "comment on issue",
_ => "comment",
};
let mut out = format!( let mut out = format!(
"[{kind}] {title}\nrepo: {repo}\nurl: {}\n\n{author}: {}\n", "[{kind}] {title}\nrepo: {repo}\nurl: {}\n\n{author}: {}\n",
if comment_html_url.is_empty() { html_url } else { comment_html_url }, if comment_html_url.is_empty() { html_url } else { comment_html_url },
@ -182,41 +174,40 @@ async fn format_notification(
out out
} else { } else {
// Notification triggered by creation or state change of the subject. // Notification triggered by creation or state change of the subject.
// let subject = fetch_json(client, subject_api_url, token).await;
// Classification uses notif["subject"]["state"] directly — Forgejo let author = subject
// returns "open" / "closed" / "merged" here. We do NOT rely on .as_ref()
// fetching the PR/issue detail for `merged`: .and_then(|s| s["user"]["login"].as_str())
// - `subject.url` points to the *issues* endpoint, which returns .unwrap_or("?");
// `pull_request.merged`, not top-level `merged`. let body = subject
// - Forgejo API type is "Pull" / "Issue", never "Pull Request". .as_ref()
let notif_state = notif["subject"]["state"].as_str().unwrap_or(""); .and_then(|s| s["body"].as_str())
.unwrap_or("")
.trim();
let state = subject
.as_ref()
.and_then(|s| s["state"].as_str())
.unwrap_or("");
let merged = subject
.as_ref()
.and_then(|s| s["merged"].as_bool())
.unwrap_or(false);
let label = notif_type_label(notif_type); let kind = match (notif_type, state, merged) {
let kind = match notif_state { ("Pull Request", "closed", true) => "PR merged".to_owned(),
"merged" => format!("{label} merged"), ("Pull Request", "closed", false) => "PR closed".to_owned(),
"closed" => format!("{label} closed"), ("Pull Request", _, _) => "new PR".to_owned(),
"open" | "" => format!("new {label}"), ("Issue", "closed", _) => "issue closed".to_owned(),
other => format!("{label}: {other}"), ("Issue", _, _) => "new issue".to_owned(),
_ => format!("new {notif_type}"),
}; };
// Fetch subject only for body/author on new (open) items — not
// worth an extra HTTP round-trip for already-closed/merged ones.
let is_open = notif_state == "open" || notif_state.is_empty();
let mut out = format!("[{kind}] {title}\nrepo: {repo}\nurl: {html_url}"); let mut out = format!("[{kind}] {title}\nrepo: {repo}\nurl: {html_url}");
if is_open { if !body.is_empty() && !state.contains("closed") && !merged {
let subject = fetch_json(client, subject_api_url, token).await; out.push_str(&format!(
let author = subject "\n\n{author}: {}",
.as_ref() truncate(body, BODY_TRUNCATE)
.and_then(|s| s["user"]["login"].as_str()) ));
.unwrap_or("?");
let body = subject
.as_ref()
.and_then(|s| s["body"].as_str())
.unwrap_or("")
.trim();
if !body.is_empty() {
out.push_str(&format!("\n\n{author}: {}", truncate(body, BODY_TRUNCATE)));
}
} }
out out
} }