From dfe847dd4356298cad7d2410bccc86d1f04f262d Mon Sep 17 00:00:00 2001 From: Sean JA Date: Thu, 5 Mar 2026 14:39:28 +0800 Subject: [PATCH 1/4] ci: removed --all-feature --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d0471ef..817e3f3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -35,7 +35,7 @@ jobs: with: components: clippy - uses: Swatinem/rust-cache@v2 - - run: cargo clippy --all-targets --all-features -- -D warnings + - run: cargo clippy --all-targets -- -D warnings test: name: Test From 78b7417ef8fe1680252cd8f0157ef3cacf7ff83e Mon Sep 17 00:00:00 2001 From: Sean JA Date: Thu, 5 Mar 2026 14:42:25 +0800 Subject: [PATCH 2/4] feat: seperate lark apps configuration --- .gitignore | 3 +- src/config.rs | 52 ++++++++++++++++++++++++++++++-- src/dispatch.rs | 16 ++++++++-- src/sinks/lark/mod.rs | 56 ++++++++++++++++++++++++++++------- src/sources/github/handler.rs | 26 ++++++++-------- 5 files changed, 124 insertions(+), 29 deletions(-) diff --git a/.gitignore b/.gitignore index 636e0e1..de37185 100644 --- a/.gitignore +++ b/.gitignore @@ -29,4 +29,5 @@ Thumbs.db # 如果你的 test-webhook.sh 里包含了真实的 URL 或 Secret, # 请取消下面这行的注释(去掉 # 号) # ------------------------------------------------------ -# test-webhook.sh \ No newline at end of file +# test-webhook.sh +payload.json \ No newline at end of file diff --git a/src/config.rs b/src/config.rs index ffe7112..43a3325 100644 --- a/src/config.rs +++ b/src/config.rs @@ -51,11 +51,26 @@ impl LinearConfig { #[derive(Debug, Default, Deserialize, Serialize)] pub struct LarkConfig { + /// Incoming webhook URL for Linear group chat notifications. #[serde(default)] pub webhook_url: String, + /// Incoming webhook URL for GitHub group chat notifications. + /// Falls back to `webhook_url` when empty. + #[serde(default)] + pub github_webhook_url: String, + /// Bot API chat ID for Linear group chat. pub target_chat_id: Option, + /// Bot API chat ID for GitHub group chat. + /// Falls back to `target_chat_id` when absent. + pub github_target_chat_id: Option, + /// App credentials for the Linear notification bot. pub app_id: Option, pub app_secret: Option, + /// App credentials for the GitHub notification bot. + /// Falls back to `app_id`/`app_secret` when absent. + pub github_app_id: Option, + pub github_app_secret: Option, + /// Verification token for the Lark unfurling/event-subscription app. pub verification_token: Option, } @@ -81,6 +96,19 @@ impl LarkConfig { target_chat_id: env.var("LARK_TARGET_CHAT_ID").ok().map(|v| v.to_string()), app_id: env.var("LARK_APP_ID").ok().map(|v| v.to_string()), app_secret: env.secret("LARK_APP_SECRET").ok().map(|s| s.to_string()), + github_webhook_url: env + .var("LARK_GITHUB_WEBHOOK_URL") + .map(|v| v.to_string()) + .unwrap_or_default(), + github_target_chat_id: env + .var("LARK_GITHUB_TARGET_CHAT_ID") + .ok() + .map(|v| v.to_string()), + github_app_id: env.var("LARK_GITHUB_APP_ID").ok().map(|v| v.to_string()), + github_app_secret: env + .secret("LARK_GITHUB_APP_SECRET") + .ok() + .map(|s| s.to_string()), verification_token: env .secret("LARK_VERIFICATION_TOKEN") .ok() @@ -93,15 +121,25 @@ impl LarkConfig { pub fn bot_client(&self, http: &Client) -> Option { match (&self.app_id, &self.app_secret) { (Some(id), Some(secret)) => { - info!("lark bot configured – Bot API notifications enabled"); + info!("LARK_APP_ID set – Linear Bot API notifications enabled"); Some(LarkBotClient::new(id.clone(), secret.clone(), http.clone())) } _ => { - info!("LARK_APP_ID/LARK_APP_SECRET not set – Bot API notifications disabled"); + info!("LARK_APP_ID/LARK_APP_SECRET not set – Linear Bot API disabled"); None } } } + + pub fn github_bot_client(&self, http: &Client) -> Option { + match (&self.github_app_id, &self.github_app_secret) { + (Some(id), Some(secret)) => { + info!("LARK_GITHUB_APP_ID set – GitHub Bot API notifications enabled"); + Some(LarkBotClient::new(id.clone(), secret.clone(), http.clone())) + } + _ => None, + } + } } fn default_alert_labels() -> Vec { @@ -261,7 +299,11 @@ pub struct AppState { pub server: ServerConfig, pub github: Option, pub http: Client, + /// Bot client for the Linear notification app. pub lark_bot: Option, + /// Bot client for the GitHub notification app. + /// Falls back to `lark_bot` when `None`. + pub github_lark_bot: Option, pub linear_client: Option, #[cfg(not(feature = "cf-worker"))] pub update_debounce: DebounceMap, @@ -279,13 +321,14 @@ impl AppState { let http = Client::new(); let lark_bot = lark.bot_client(&http); + let github_lark_bot = lark.github_bot_client(&http); let linear_client = linear.graphql_client(&http); if lark.verification_token.is_some() { info!("LARK_VERIFICATION_TOKEN set – event verification enabled"); } if lark.target_chat_id.is_some() { - info!("LARK_TARGET_CHAT_ID set – Bot API group chat enabled"); + info!("LARK_TARGET_CHAT_ID set – Linear Bot API group chat enabled"); } if let Some(gh) = &github { info!("GITHUB_WEBHOOK_SECRET set – GitHub webhook source enabled"); @@ -305,6 +348,7 @@ impl AppState { github, http, lark_bot, + github_lark_bot, linear_client, update_debounce: DebounceMap::new(), } @@ -321,6 +365,7 @@ impl AppState { let http = Client::new(); let lark_bot = lark.bot_client(&http); + let github_lark_bot = lark.github_bot_client(&http); let linear_client = linear.graphql_client(&http); Self { @@ -330,6 +375,7 @@ impl AppState { github, http, lark_bot, + github_lark_bot, linear_client, env, } diff --git a/src/dispatch.rs b/src/dispatch.rs index 65af8c4..48dcbb1 100644 --- a/src/dispatch.rs +++ b/src/dispatch.rs @@ -2,8 +2,8 @@ use crate::{config::AppState, event::Event, sinks}; -/// Sends `event` to all sinks. If `dm_email` is provided, a direct message -/// is also sent to that address. +/// Sends `event` to the Linear Lark group. If `dm_email` is provided, a +/// direct message is also sent using the Linear bot credentials. pub async fn dispatch(event: &Event, state: &AppState, dm_email: Option<&str>) { sinks::lark::notify(event, state).await; @@ -11,3 +11,15 @@ pub async fn dispatch(event: &Event, state: &AppState, dm_email: Option<&str>) { sinks::lark::try_dm(event, bot, email).await; } } + +/// Sends `event` to the GitHub Lark group. If `dm_email` is provided, a +/// direct message is sent using the GitHub bot credentials (falling back to +/// the Linear bot when no GitHub-specific bot is configured). +pub async fn dispatch_github(event: &Event, state: &AppState, dm_email: Option<&str>) { + sinks::lark::notify_github(event, state).await; + + let bot = state.github_lark_bot.as_ref().or(state.lark_bot.as_ref()); + if let (Some(email), Some(bot)) = (dm_email, bot) { + sinks::lark::try_dm(event, bot, email).await; + } +} diff --git a/src/sinks/lark/mod.rs b/src/sinks/lark/mod.rs index b8450ac..4b818de 100644 --- a/src/sinks/lark/mod.rs +++ b/src/sinks/lark/mod.rs @@ -13,26 +13,62 @@ use tracing::error; use crate::{config::AppState, event::Event}; -/// Sends a card notification for `event` to the Lark group. +/// Sends a card notification for `event` to the Linear Lark group. /// -/// Prefers Bot API (`target_chat_id`) when available, falls back to the -/// simple webhook (`webhook_url`). +/// Prefers Bot API (`target_chat_id`) when available, falls back to +/// `webhook_url`. pub async fn notify(event: &Event, state: &AppState) { let card = cards::build_lark_card(event); + deliver( + &card, + &state.http, + state.lark_bot.as_ref(), + state.lark.target_chat_id.as_deref(), + &state.lark.webhook_url, + "Linear", + ) + .await; +} + +/// Sends a card notification for `event` to the GitHub Lark group. +/// +/// Uses `LARK_GITHUB_*` credentials/webhook when set, otherwise falls back +/// to the shared Linear Lark app and `LARK_WEBHOOK_URL`. +pub async fn notify_github(event: &Event, state: &AppState) { + let card = cards::build_lark_card(event); + let bot = state.github_lark_bot.as_ref().or(state.lark_bot.as_ref()); + let chat_id = state + .lark + .github_target_chat_id + .as_deref() + .or(state.lark.target_chat_id.as_deref()); + let webhook = if !state.lark.github_webhook_url.is_empty() { + &state.lark.github_webhook_url + } else { + &state.lark.webhook_url + }; + deliver(&card, &state.http, bot, chat_id, webhook, "GitHub").await; +} - match (&state.lark_bot, &state.lark.target_chat_id) { +async fn deliver( + card: &models::LarkMessage, + http: &reqwest::Client, + bot: Option<&LarkBotClient>, + chat_id: Option<&str>, + webhook_url: &str, + source: &str, +) { + match (bot, chat_id) { (Some(bot), Some(chat_id)) => { if let Err(e) = bot.send_to_chat(chat_id, &card.card).await { - error!("failed to send card to chat {chat_id}: {e}"); + error!("failed to send {source} card to chat {chat_id}: {e}"); } } - _ if !state.lark.webhook_url.is_empty() => { - webhook::send_lark_card(&state.http, &state.lark.webhook_url, &card).await; + _ if !webhook_url.is_empty() => { + webhook::send_lark_card(http, webhook_url, card).await; } _ => { - error!( - "no Lark delivery method configured (need LARK_TARGET_CHAT_ID + bot, or LARK_WEBHOOK_URL)" - ); + error!("no Lark delivery method configured for {source} events"); } } } diff --git a/src/sources/github/handler.rs b/src/sources/github/handler.rs index c57cdee..8c0d6a6 100644 --- a/src/sources/github/handler.rs +++ b/src/sources/github/handler.rs @@ -345,7 +345,7 @@ async fn handle_pull_request( deletions: pr.deletions.unwrap_or(0), url: html_url, }; - dispatch::dispatch(&event, state, None).await; + dispatch::dispatch_github(&event, state, None).await; StatusCode::OK } PullRequestWebhookEventAction::ReviewRequested => { @@ -368,7 +368,7 @@ async fn handle_pull_request( reviewer_lark_id, url: html_url, }; - dispatch::dispatch(&event, state, dm_email.as_deref()).await; + dispatch::dispatch_github(&event, state, dm_email.as_deref()).await; StatusCode::OK } PullRequestWebhookEventAction::Closed if pr.merged_at.is_some() => { @@ -386,7 +386,7 @@ async fn handle_pull_request( merged_by, url: html_url, }; - dispatch::dispatch(&event, state, None).await; + dispatch::dispatch_github(&event, state, None).await; StatusCode::OK } _ => { @@ -429,7 +429,7 @@ async fn handle_issues( author, url: html_url, }; - dispatch::dispatch(&event, state, None).await; + dispatch::dispatch_github(&event, state, None).await; StatusCode::OK } @@ -465,7 +465,7 @@ async fn handle_push( commits, compare_url: payload.compare.to_string(), }; - dispatch::dispatch(&event, state, None).await; + dispatch::dispatch_github(&event, state, None).await; StatusCode::OK } @@ -631,7 +631,7 @@ async fn handle_pr_cf( deletions: pr.deletions.unwrap_or(0), url: html_url, }; - dispatch::dispatch(&event, state, None).await; + dispatch::dispatch_github(&event, state, None).await; StatusCode::OK } "review_requested" => { @@ -654,7 +654,7 @@ async fn handle_pr_cf( reviewer_lark_id, url: html_url, }; - dispatch::dispatch(&event, state, dm_email.as_deref()).await; + dispatch::dispatch_github(&event, state, dm_email.as_deref()).await; StatusCode::OK } "closed" if pr.merged_at.is_some() => { @@ -672,7 +672,7 @@ async fn handle_pr_cf( merged_by, url: html_url, }; - dispatch::dispatch(&event, state, None).await; + dispatch::dispatch_github(&event, state, None).await; StatusCode::OK } _ => { @@ -714,7 +714,7 @@ async fn handle_issues_cf( author: issue.user.login.clone(), url: issue.html_url.clone(), }; - dispatch::dispatch(&event, state, None).await; + dispatch::dispatch_github(&event, state, None).await; StatusCode::OK } @@ -750,7 +750,7 @@ async fn handle_push_cf( commits, compare_url: payload.compare.clone(), }; - dispatch::dispatch(&event, state, None).await; + dispatch::dispatch_github(&event, state, None).await; StatusCode::OK } @@ -789,7 +789,7 @@ async fn dispatch_workflow_run( conclusion, url: run.html_url, }; - dispatch::dispatch(&event, state, None).await; + dispatch::dispatch_github(&event, state, None).await; StatusCode::OK } @@ -815,7 +815,7 @@ async fn dispatch_secret_scanning( secret_type: secret_type.to_string(), url: alert.html_url, }; - dispatch::dispatch(&event, state, None).await; + dispatch::dispatch_github(&event, state, None).await; StatusCode::OK } @@ -855,7 +855,7 @@ async fn dispatch_dependabot( summary: summary.to_string(), url: alert.html_url, }; - dispatch::dispatch(&event, state, None).await; + dispatch::dispatch_github(&event, state, None).await; StatusCode::OK } From 6cb3d66f10551e26102421aeafad79e7199265d7 Mon Sep 17 00:00:00 2001 From: Sean JA Date: Thu, 5 Mar 2026 15:33:56 +0800 Subject: [PATCH 3/4] fix: return 200 on parse failures for CF Worker debug --- src/sources/github/handler.rs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/sources/github/handler.rs b/src/sources/github/handler.rs index 8c0d6a6..45b088c 100644 --- a/src/sources/github/handler.rs +++ b/src/sources/github/handler.rs @@ -526,7 +526,7 @@ async fn dispatch_cf( Ok(p) => p, Err(e) => { warn!("failed to parse pull_request payload: {e}"); - return StatusCode::BAD_REQUEST; + return StatusCode::OK; } }; handle_pr_cf(state, github, repo, payload).await @@ -536,7 +536,7 @@ async fn dispatch_cf( Ok(p) => p, Err(e) => { warn!("failed to parse issues payload: {e}"); - return StatusCode::BAD_REQUEST; + return StatusCode::OK; } }; handle_issues_cf(state, github, repo, payload).await @@ -546,7 +546,7 @@ async fn dispatch_cf( Ok(p) => p, Err(e) => { warn!("failed to parse push payload: {e}"); - return StatusCode::BAD_REQUEST; + return StatusCode::OK; } }; handle_push_cf(state, repo, payload).await @@ -556,7 +556,7 @@ async fn dispatch_cf( Ok(p) => p, Err(e) => { warn!("failed to parse workflow_run payload: {e}"); - return StatusCode::BAD_REQUEST; + return StatusCode::OK; } }; if payload.action != "completed" { @@ -570,7 +570,7 @@ async fn dispatch_cf( Ok(p) => p, Err(e) => { warn!("failed to parse secret_scanning_alert payload: {e}"); - return StatusCode::BAD_REQUEST; + return StatusCode::OK; } }; if payload.action != "created" { @@ -584,7 +584,7 @@ async fn dispatch_cf( Ok(p) => p, Err(e) => { warn!("failed to parse dependabot_alert payload: {e}"); - return StatusCode::BAD_REQUEST; + return StatusCode::OK; } }; if payload.action != "created" { @@ -769,7 +769,7 @@ async fn dispatch_workflow_run( Ok(r) => r, Err(e) => { warn!("failed to parse workflow_run data: {e}"); - return StatusCode::BAD_REQUEST; + return StatusCode::OK; } }; let conclusion = run.conclusion.unwrap_or_else(|| "unknown".to_string()); @@ -802,7 +802,7 @@ async fn dispatch_secret_scanning( Ok(a) => a, Err(e) => { warn!("failed to parse secret_scanning_alert data: {e}"); - return StatusCode::BAD_REQUEST; + return StatusCode::OK; } }; let secret_type = alert @@ -828,7 +828,7 @@ async fn dispatch_dependabot( Ok(a) => a, Err(e) => { warn!("failed to parse dependabot_alert data: {e}"); - return StatusCode::BAD_REQUEST; + return StatusCode::OK; } }; let severity = alert.severity.to_lowercase(); From a2cc0de64080470232c017d01c06c3876ee489d1 Mon Sep 17 00:00:00 2001 From: Sean JA Date: Thu, 5 Mar 2026 17:54:19 +0800 Subject: [PATCH 4/4] fix: corrected secret names --- src/config.rs | 55 +++++++++++++++----------------------- src/debounce_do.rs | 61 +++++++++++++++---------------------------- src/sinks/lark/mod.rs | 61 ++++++++++--------------------------------- wrangler.toml | 1 - 4 files changed, 56 insertions(+), 122 deletions(-) diff --git a/src/config.rs b/src/config.rs index 43a3325..dbf0a57 100644 --- a/src/config.rs +++ b/src/config.rs @@ -58,19 +58,14 @@ pub struct LarkConfig { /// Falls back to `webhook_url` when empty. #[serde(default)] pub github_webhook_url: String, - /// Bot API chat ID for Linear group chat. - pub target_chat_id: Option, - /// Bot API chat ID for GitHub group chat. - /// Falls back to `target_chat_id` when absent. - pub github_target_chat_id: Option, - /// App credentials for the Linear notification bot. + /// Enterprise self-built app credentials — used only for Linear DMs. pub app_id: Option, pub app_secret: Option, - /// App credentials for the GitHub notification bot. + /// Enterprise self-built app credentials — used only for GitHub review-request DMs. /// Falls back to `app_id`/`app_secret` when absent. pub github_app_id: Option, pub github_app_secret: Option, - /// Verification token for the Lark unfurling/event-subscription app. + /// Verification token for the Lark URL-unfurling event-subscription app. pub verification_token: Option, } @@ -90,20 +85,15 @@ impl LarkConfig { pub fn from_worker_env(env: &worker::Env) -> Result { Ok(Self { webhook_url: env - .var("LARK_WEBHOOK_URL") - .map(|v| v.to_string()) + .secret("LARK_WEBHOOK_URL") + .map(|s| s.to_string()) .unwrap_or_default(), - target_chat_id: env.var("LARK_TARGET_CHAT_ID").ok().map(|v| v.to_string()), - app_id: env.var("LARK_APP_ID").ok().map(|v| v.to_string()), - app_secret: env.secret("LARK_APP_SECRET").ok().map(|s| s.to_string()), github_webhook_url: env - .var("LARK_GITHUB_WEBHOOK_URL") - .map(|v| v.to_string()) + .secret("LARK_GITHUB_WEBHOOK_URL") + .map(|s| s.to_string()) .unwrap_or_default(), - github_target_chat_id: env - .var("LARK_GITHUB_TARGET_CHAT_ID") - .ok() - .map(|v| v.to_string()), + app_id: env.var("LARK_APP_ID").ok().map(|v| v.to_string()), + app_secret: env.secret("LARK_APP_SECRET").ok().map(|s| s.to_string()), github_app_id: env.var("LARK_GITHUB_APP_ID").ok().map(|v| v.to_string()), github_app_secret: env .secret("LARK_GITHUB_APP_SECRET") @@ -118,23 +108,23 @@ impl LarkConfig { } impl LarkConfig { - pub fn bot_client(&self, http: &Client) -> Option { + /// Creates the Linear DM bot client (enterprise self-built app, DMs only). + pub fn linear_dm_bot(&self, http: &Client) -> Option { match (&self.app_id, &self.app_secret) { (Some(id), Some(secret)) => { - info!("LARK_APP_ID set – Linear Bot API notifications enabled"); + info!("LARK_APP_ID set – Linear DM bot enabled"); Some(LarkBotClient::new(id.clone(), secret.clone(), http.clone())) } - _ => { - info!("LARK_APP_ID/LARK_APP_SECRET not set – Linear Bot API disabled"); - None - } + _ => None, } } - pub fn github_bot_client(&self, http: &Client) -> Option { + /// Creates the GitHub DM bot client (enterprise self-built app, DMs only). + /// Falls back to the Linear DM bot when `LARK_GITHUB_APP_ID` is absent. + pub fn github_dm_bot(&self, http: &Client) -> Option { match (&self.github_app_id, &self.github_app_secret) { (Some(id), Some(secret)) => { - info!("LARK_GITHUB_APP_ID set – GitHub Bot API notifications enabled"); + info!("LARK_GITHUB_APP_ID set – GitHub DM bot enabled"); Some(LarkBotClient::new(id.clone(), secret.clone(), http.clone())) } _ => None, @@ -320,16 +310,13 @@ impl AppState { let github = GitHubConfig::from_env(); let http = Client::new(); - let lark_bot = lark.bot_client(&http); - let github_lark_bot = lark.github_bot_client(&http); + let lark_bot = lark.linear_dm_bot(&http); + let github_lark_bot = lark.github_dm_bot(&http); let linear_client = linear.graphql_client(&http); if lark.verification_token.is_some() { info!("LARK_VERIFICATION_TOKEN set – event verification enabled"); } - if lark.target_chat_id.is_some() { - info!("LARK_TARGET_CHAT_ID set – Linear Bot API group chat enabled"); - } if let Some(gh) = &github { info!("GITHUB_WEBHOOK_SECRET set – GitHub webhook source enabled"); if !gh.repo_whitelist.is_empty() { @@ -364,8 +351,8 @@ impl AppState { let github = GitHubConfig::from_worker_env(&env); let http = Client::new(); - let lark_bot = lark.bot_client(&http); - let github_lark_bot = lark.github_bot_client(&http); + let lark_bot = lark.linear_dm_bot(&http); + let github_lark_bot = lark.github_dm_bot(&http); let linear_client = linear.graphql_client(&http); Self { diff --git a/src/debounce_do.rs b/src/debounce_do.rs index b648b94..8776452 100644 --- a/src/debounce_do.rs +++ b/src/debounce_do.rs @@ -85,50 +85,31 @@ impl DurableObject for DebounceObject { let http = reqwest::Client::new(); - // Build the card once, then deliver via Bot API or webhook fallback. + // Deliver to the Linear group chat via incoming webhook. let card = crate::sinks::lark::cards::build_lark_card(&event); - - let app_id = self.env.var("LARK_APP_ID").ok().map(|v| v.to_string()); - let app_secret = self - .env - .secret("LARK_APP_SECRET") - .ok() - .map(|s| s.to_string()); - let target_chat_id = self + let webhook_url = self .env - .var("LARK_TARGET_CHAT_ID") - .ok() - .map(|v| v.to_string()); - - let bot = match (app_id, app_secret) { - (Some(id), Some(secret)) => Some(crate::sinks::lark::LarkBotClient::new( - id, - secret, - http.clone(), - )), - _ => None, - }; - - match (&bot, &target_chat_id) { - (Some(b), Some(chat_id)) => { - if let Err(e) = b.send_to_chat(chat_id, &card.card).await { - worker::console_error!("failed to send card to chat: {e}"); - } - } - _ => { - let webhook_url = self - .env - .var("LARK_WEBHOOK_URL") - .map(|v| v.to_string()) - .unwrap_or_default(); - if !webhook_url.is_empty() { - crate::sinks::lark::webhook::send_lark_card(&http, &webhook_url, &card).await; - } - } + .var("LARK_WEBHOOK_URL") + .map(|v| v.to_string()) + .unwrap_or_default(); + if !webhook_url.is_empty() { + crate::sinks::lark::webhook::send_lark_card(&http, &webhook_url, &card).await; + } else { + worker::console_error!("LARK_WEBHOOK_URL not configured — group notification skipped"); } - if let (Some(email), Some(b)) = (&dm_email, &bot) { - crate::sinks::lark::try_dm(&event, b, email).await; + // Send DM via the Linear DM bot (enterprise self-built app). + if let Some(email) = &dm_email { + let app_id = self.env.var("LARK_APP_ID").ok().map(|v| v.to_string()); + let app_secret = self + .env + .secret("LARK_APP_SECRET") + .ok() + .map(|s| s.to_string()); + if let (Some(id), Some(secret)) = (app_id, app_secret) { + let bot = crate::sinks::lark::LarkBotClient::new(id, secret, http.clone()); + crate::sinks::lark::try_dm(&event, &bot, email).await; + } } Response::ok("dispatched") diff --git a/src/sinks/lark/mod.rs b/src/sinks/lark/mod.rs index 4b818de..3b7994f 100644 --- a/src/sinks/lark/mod.rs +++ b/src/sinks/lark/mod.rs @@ -13,68 +13,35 @@ use tracing::error; use crate::{config::AppState, event::Event}; -/// Sends a card notification for `event` to the Linear Lark group. -/// -/// Prefers Bot API (`target_chat_id`) when available, falls back to -/// `webhook_url`. +/// Sends a card notification for `event` to the Linear group chat via webhook. pub async fn notify(event: &Event, state: &AppState) { let card = cards::build_lark_card(event); - deliver( - &card, - &state.http, - state.lark_bot.as_ref(), - state.lark.target_chat_id.as_deref(), - &state.lark.webhook_url, - "Linear", - ) - .await; + if !state.lark.webhook_url.is_empty() { + webhook::send_lark_card(&state.http, &state.lark.webhook_url, &card).await; + } else { + error!("LARK_WEBHOOK_URL not configured — Linear group chat notification skipped"); + } } -/// Sends a card notification for `event` to the GitHub Lark group. +/// Sends a card notification for `event` to the GitHub group chat via webhook. /// -/// Uses `LARK_GITHUB_*` credentials/webhook when set, otherwise falls back -/// to the shared Linear Lark app and `LARK_WEBHOOK_URL`. +/// Uses `LARK_GITHUB_WEBHOOK_URL` when set, falls back to `LARK_WEBHOOK_URL`. pub async fn notify_github(event: &Event, state: &AppState) { let card = cards::build_lark_card(event); - let bot = state.github_lark_bot.as_ref().or(state.lark_bot.as_ref()); - let chat_id = state - .lark - .github_target_chat_id - .as_deref() - .or(state.lark.target_chat_id.as_deref()); let webhook = if !state.lark.github_webhook_url.is_empty() { &state.lark.github_webhook_url } else { &state.lark.webhook_url }; - deliver(&card, &state.http, bot, chat_id, webhook, "GitHub").await; -} - -async fn deliver( - card: &models::LarkMessage, - http: &reqwest::Client, - bot: Option<&LarkBotClient>, - chat_id: Option<&str>, - webhook_url: &str, - source: &str, -) { - match (bot, chat_id) { - (Some(bot), Some(chat_id)) => { - if let Err(e) = bot.send_to_chat(chat_id, &card.card).await { - error!("failed to send {source} card to chat {chat_id}: {e}"); - } - } - _ if !webhook_url.is_empty() => { - webhook::send_lark_card(http, webhook_url, card).await; - } - _ => { - error!("no Lark delivery method configured for {source} events"); - } + if !webhook.is_empty() { + webhook::send_lark_card(&state.http, webhook, &card).await; + } else { + error!("no webhook URL configured — GitHub group chat notification skipped"); } } -/// DMs the assignee about `event` (no-op when `bot` is `None` or event -/// does not support DM notifications). +/// Sends a DM about `event` via the enterprise self-built app bot. +/// No-op when the event does not support DM notifications. pub async fn try_dm(event: &Event, bot: &LarkBotClient, email: &str) { if let Some(card) = cards::build_assign_dm_card(event) && let Err(e) = bot.send_dm(email, &card).await diff --git a/wrangler.toml b/wrangler.toml index e9eb6a1..a2fb84f 100644 --- a/wrangler.toml +++ b/wrangler.toml @@ -13,6 +13,5 @@ tag = "v1" new_sqlite_classes = ["DebounceObject"] [vars] -LARK_WEBHOOK_URL = "https://open.larksuite.com/open-apis/bot/v2/hook/c10edb2b-d186-457e-926d-7f4562846983" PORT = "3000" DEBOUNCE_DELAY_MS = "5000"