fix: use try_from for i64/u64 casts; split format_notification into helpers
This commit is contained in:
parent
484cea62c7
commit
bbe2112dc9
3 changed files with 168 additions and 141 deletions
|
|
@ -211,7 +211,6 @@ fn review_state_label(state: &str) -> Option<&str> {
|
||||||
///
|
///
|
||||||
/// Number is extracted from `html_url` last path segment before any `#`.
|
/// Number is extracted from `html_url` last path segment before any `#`.
|
||||||
/// Repo slug (`owner/name`) is always included — agents may watch multiple repos.
|
/// Repo slug (`owner/name`) is always included — agents may watch multiple repos.
|
||||||
#[allow(clippy::too_many_lines)] // multiple notification types handled in one place by design
|
|
||||||
async fn format_notification(
|
async fn format_notification(
|
||||||
client: &reqwest::Client,
|
client: &reqwest::Client,
|
||||||
token: &str,
|
token: &str,
|
||||||
|
|
@ -256,11 +255,37 @@ async fn format_notification(
|
||||||
};
|
};
|
||||||
|
|
||||||
let is_pr = matches!(notif_type, "Pull Request" | "Pull");
|
let is_pr = matches!(notif_type, "Pull Request" | "Pull");
|
||||||
|
let meta_suffix = build_meta_suffix(subject.as_ref(), is_pr);
|
||||||
|
|
||||||
// Build assignee + reviewer suffix appended to all notification kinds.
|
// Determine whether this notification was triggered by a comment/review or
|
||||||
let meta_suffix = {
|
// by creation/state-change of the subject itself.
|
||||||
|
let has_comment = !comment_api_url.is_empty() && comment_api_url != subject_api_url;
|
||||||
|
|
||||||
|
let meta = NotifMeta { title, notif_type, html_url, num, repo, meta_suffix, subject, is_pr };
|
||||||
|
if has_comment {
|
||||||
|
format_comment_notification(client, token, &meta, comment_api_url, comment_html_url, own_login).await
|
||||||
|
} else {
|
||||||
|
format_state_change_notification(notif, &meta, own_login)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Shared notification metadata extracted from the raw Forgejo JSON.
|
||||||
|
struct NotifMeta<'a> {
|
||||||
|
title: &'a str,
|
||||||
|
notif_type: &'a str,
|
||||||
|
html_url: &'a str,
|
||||||
|
num: String,
|
||||||
|
repo: String,
|
||||||
|
meta_suffix: String,
|
||||||
|
/// Fetched subject detail (issue/PR JSON); used for review-request detection.
|
||||||
|
subject: Option<serde_json::Value>,
|
||||||
|
is_pr: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build the `\nassignee: ...` (and optionally `\nreviewer: ...`) suffix
|
||||||
|
/// appended to all notification kinds.
|
||||||
|
fn build_meta_suffix(subject: Option<&serde_json::Value>, is_pr: bool) -> String {
|
||||||
let assignees: Vec<&str> = subject
|
let assignees: Vec<&str> = subject
|
||||||
.as_ref()
|
|
||||||
.and_then(|s| s["assignees"].as_array())
|
.and_then(|s| s["assignees"].as_array())
|
||||||
.map(|arr| arr.iter().filter_map(|a| a["login"].as_str()).collect())
|
.map(|arr| arr.iter().filter_map(|a| a["login"].as_str()).collect())
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
|
@ -272,15 +297,10 @@ async fn format_notification(
|
||||||
// For PRs, include requested_reviewers when present.
|
// For PRs, include requested_reviewers when present.
|
||||||
let reviewer_line = if is_pr {
|
let reviewer_line = if is_pr {
|
||||||
let reviewers: Vec<&str> = subject
|
let reviewers: Vec<&str> = subject
|
||||||
.as_ref()
|
|
||||||
.and_then(|s| s["requested_reviewers"].as_array())
|
.and_then(|s| s["requested_reviewers"].as_array())
|
||||||
.map(|arr| arr.iter().filter_map(|r| r["login"].as_str()).collect())
|
.map(|arr| arr.iter().filter_map(|r| r["login"].as_str()).collect())
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
if reviewers.is_empty() {
|
if reviewers.is_empty() { None } else { Some(format!("reviewer: {}", reviewers.join(", "))) }
|
||||||
None
|
|
||||||
} else {
|
|
||||||
Some(format!("reviewer: {}", reviewers.join(", ")))
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|
@ -288,14 +308,17 @@ async fn format_notification(
|
||||||
Some(r) => format!("\n{assignee_line}\n{r}"),
|
Some(r) => format!("\n{assignee_line}\n{r}"),
|
||||||
None => format!("\n{assignee_line}"),
|
None => format!("\n{assignee_line}"),
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
// Determine whether this notification was triggered by a comment/review or
|
/// Format a notification triggered by a new comment or review submission.
|
||||||
// by creation/state-change of the subject itself.
|
async fn format_comment_notification(
|
||||||
let has_comment = !comment_api_url.is_empty() && comment_api_url != subject_api_url;
|
client: &reqwest::Client,
|
||||||
|
token: &str,
|
||||||
if has_comment {
|
meta: &NotifMeta<'_>,
|
||||||
// Notification triggered by a new comment or review submission.
|
comment_api_url: &str,
|
||||||
|
comment_html_url: &str,
|
||||||
|
own_login: &str,
|
||||||
|
) -> Option<String> {
|
||||||
let payload = fetch_json(client, comment_api_url, token).await;
|
let payload = fetch_json(client, comment_api_url, token).await;
|
||||||
|
|
||||||
let actor_login = payload
|
let actor_login = payload
|
||||||
|
|
@ -324,8 +347,9 @@ async fn format_notification(
|
||||||
.and_then(|c| c["state"].as_str())
|
.and_then(|c| c["state"].as_str())
|
||||||
.and_then(review_state_label);
|
.and_then(review_state_label);
|
||||||
|
|
||||||
let url = if comment_html_url.is_empty() { html_url } else { comment_html_url };
|
let url = if comment_html_url.is_empty() { meta.html_url } else { comment_html_url };
|
||||||
let author = if actor_login.is_empty() { "?" } else { actor_login };
|
let author = if actor_login.is_empty() { "?" } else { actor_login };
|
||||||
|
let NotifMeta { title, notif_type, num, repo, meta_suffix, .. } = meta;
|
||||||
|
|
||||||
if let Some(review_label) = review_state {
|
if let Some(review_label) = review_state {
|
||||||
// Review submission on a PR.
|
// Review submission on a PR.
|
||||||
|
|
@ -336,7 +360,7 @@ async fn format_notification(
|
||||||
} else {
|
} else {
|
||||||
write!(out, "\n\n{author}: {}", truncate(body_text, BODY_TRUNCATE)).ok();
|
write!(out, "\n\n{author}: {}", truncate(body_text, BODY_TRUNCATE)).ok();
|
||||||
}
|
}
|
||||||
out.push_str(&meta_suffix);
|
out.push_str(meta_suffix);
|
||||||
Some(out)
|
Some(out)
|
||||||
} else {
|
} else {
|
||||||
// Regular comment.
|
// Regular comment.
|
||||||
|
|
@ -348,12 +372,17 @@ async fn format_notification(
|
||||||
if out.ends_with('\n') {
|
if out.ends_with('\n') {
|
||||||
out.pop();
|
out.pop();
|
||||||
}
|
}
|
||||||
out.push_str(&meta_suffix);
|
out.push_str(meta_suffix);
|
||||||
Some(out)
|
Some(out)
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
// Notification triggered by creation or state change of the subject.
|
|
||||||
//
|
/// Format a notification triggered by creation or state change of the subject.
|
||||||
|
fn format_state_change_notification(
|
||||||
|
notif: &serde_json::Value,
|
||||||
|
meta: &NotifMeta<'_>,
|
||||||
|
own_login: &str,
|
||||||
|
) -> Option<String> {
|
||||||
// Classification uses notif["subject"]["state"] directly — Forgejo
|
// Classification uses notif["subject"]["state"] directly — Forgejo
|
||||||
// returns "open" / "closed" / "merged" here. We do NOT rely on
|
// returns "open" / "closed" / "merged" here. We do NOT rely on
|
||||||
// fetching the PR/issue detail for `merged`:
|
// fetching the PR/issue detail for `merged`:
|
||||||
|
|
@ -373,6 +402,7 @@ async fn format_notification(
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let NotifMeta { title, notif_type, html_url, num, repo, meta_suffix, subject, is_pr } = meta;
|
||||||
let label = notif_type_label(notif_type);
|
let label = notif_type_label(notif_type);
|
||||||
let kind = match notif_state {
|
let kind = match notif_state {
|
||||||
"merged" => format!("{label} merged{num}{repo}"),
|
"merged" => format!("{label} merged{num}{repo}"),
|
||||||
|
|
@ -382,19 +412,18 @@ async fn format_notification(
|
||||||
};
|
};
|
||||||
|
|
||||||
// Review-request detection (#253): Forgejo does not always set
|
// Review-request detection (#253): Forgejo does not always set
|
||||||
// reason == "review_requested" (observed reason is null). Check
|
// reason == "review_requested" (observed as null). Check
|
||||||
// requested_reviewers instead, which is reliable. If own_login is
|
// requested_reviewers instead, which is reliable. If own_login is
|
||||||
// in the list, this is a review request -- override the kind.
|
// in the list, override the kind.
|
||||||
// `subject` and `is_pr` are already fetched unconditionally above (#256).
|
// subject and is_pr are already fetched unconditionally above (#256).
|
||||||
let is_review_request = is_new
|
let is_review_request = is_new
|
||||||
&& is_pr
|
&& *is_pr
|
||||||
&& !own_login.is_empty()
|
&& !own_login.is_empty()
|
||||||
&& subject
|
&& subject
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.and_then(|s| s["requested_reviewers"].as_array())
|
.and_then(|s| s["requested_reviewers"].as_array())
|
||||||
.map(|arr| arr.iter().any(|r| r["login"].as_str() == Some(own_login)))
|
.map(|arr| arr.iter().any(|r| r["login"].as_str() == Some(own_login)))
|
||||||
.unwrap_or(false);
|
.unwrap_or(false);
|
||||||
|
|
||||||
let kind = if is_review_request {
|
let kind = if is_review_request {
|
||||||
format!("review requested{num}{repo}")
|
format!("review requested{num}{repo}")
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -402,10 +431,9 @@ async fn format_notification(
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut out = format!("[{kind}] {title}\nurl: {html_url}");
|
let mut out = format!("[{kind}] {title}\nurl: {html_url}");
|
||||||
out.push_str(&meta_suffix);
|
out.push_str(meta_suffix);
|
||||||
Some(out)
|
Some(out)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
async fn poll_once(
|
async fn poll_once(
|
||||||
|
|
|
||||||
|
|
@ -335,8 +335,7 @@ async fn api_stats(
|
||||||
// Pass the window span to the reminder-stats RPC so the broker
|
// Pass the window span to the reminder-stats RPC so the broker
|
||||||
// filters its counts to the same time range as the chart data.
|
// filters its counts to the same time range as the chart data.
|
||||||
let window_secs = window.span_secs();
|
let window_secs = window.span_secs();
|
||||||
#[allow(clippy::cast_sign_loss)] // window span is always a positive duration
|
let window_secs_u = u64::try_from(window_secs).unwrap_or(0);
|
||||||
let window_secs_u = window_secs.max(0) as u64;
|
|
||||||
snapshot.reminder_stats = fetch_reminder_stats(&state.socket, state.flavor(), window_secs_u).await;
|
snapshot.reminder_stats = fetch_reminder_stats(&state.socket, state.flavor(), window_secs_u).await;
|
||||||
axum::Json(snapshot)
|
axum::Json(snapshot)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -609,13 +609,13 @@ impl Broker {
|
||||||
/// in the last `since_secs` seconds (0 = all reminders).
|
/// in the last `since_secs` seconds (0 = all reminders).
|
||||||
pub fn reminder_rollup_for(&self, agent: &str, since_secs: u64) -> Result<hive_sh4re::ReminderStats> {
|
pub fn reminder_rollup_for(&self, agent: &str, since_secs: u64) -> Result<hive_sh4re::ReminderStats> {
|
||||||
let conn = self.conn.lock().unwrap();
|
let conn = self.conn.lock().unwrap();
|
||||||
#[allow(clippy::cast_possible_wrap)] // unix epoch secs fit in i64 until year 292B
|
|
||||||
let cutoff_time = if since_secs > 0 {
|
let cutoff_time = if since_secs > 0 {
|
||||||
let now = std::time::SystemTime::now()
|
let now = std::time::SystemTime::now()
|
||||||
.duration_since(std::time::UNIX_EPOCH)
|
.duration_since(std::time::UNIX_EPOCH)
|
||||||
.ok()
|
.ok()
|
||||||
.map_or(0, |d| d.as_secs() as i64);
|
.and_then(|d| i64::try_from(d.as_secs()).ok())
|
||||||
now - since_secs as i64
|
.unwrap_or(0);
|
||||||
|
now.saturating_sub(i64::try_from(since_secs).unwrap_or(i64::MAX))
|
||||||
} else {
|
} else {
|
||||||
i64::MIN
|
i64::MIN
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue