Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,5 @@ Thumbs.db
# 如果你的 test-webhook.sh 里包含了真实的 URL 或 Secret,
# 请取消下面这行的注释(去掉 # 号)
# ------------------------------------------------------
# test-webhook.sh
# test-webhook.sh
payload.json
61 changes: 47 additions & 14 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,11 +51,21 @@ impl LinearConfig {

#[derive(Debug, Default, Deserialize, Serialize)]
pub struct LarkConfig {
/// Incoming webhook URL for Linear group chat notifications.
#[serde(default)]
pub webhook_url: String,
pub target_chat_id: Option<String>,
/// Incoming webhook URL for GitHub group chat notifications.
/// Falls back to `webhook_url` when empty.
#[serde(default)]
pub github_webhook_url: String,
/// Enterprise self-built app credentials — used only for Linear DMs.
pub app_id: Option<String>,
pub app_secret: Option<String>,
/// 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<String>,
pub github_app_secret: Option<String>,
/// Verification token for the Lark URL-unfurling event-subscription app.
pub verification_token: Option<String>,
}

Expand All @@ -75,12 +85,20 @@ impl LarkConfig {
pub fn from_worker_env(env: &worker::Env) -> Result<Self, String> {
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(),
github_webhook_url: env
.secret("LARK_GITHUB_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_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()
Expand All @@ -90,16 +108,26 @@ impl LarkConfig {
}

impl LarkConfig {
pub fn bot_client(&self, http: &Client) -> Option<LarkBotClient> {
/// Creates the Linear DM bot client (enterprise self-built app, DMs only).
pub fn linear_dm_bot(&self, http: &Client) -> Option<LarkBotClient> {
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 DM bot enabled");
Some(LarkBotClient::new(id.clone(), secret.clone(), http.clone()))
}
_ => {
info!("LARK_APP_ID/LARK_APP_SECRET not set – Bot API notifications disabled");
None
_ => None,
}
}

/// 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<LarkBotClient> {
match (&self.github_app_id, &self.github_app_secret) {
(Some(id), Some(secret)) => {
info!("LARK_GITHUB_APP_ID set – GitHub DM bot enabled");
Some(LarkBotClient::new(id.clone(), secret.clone(), http.clone()))
}
_ => None,
}
}
}
Expand Down Expand Up @@ -261,7 +289,11 @@ pub struct AppState {
pub server: ServerConfig,
pub github: Option<GitHubConfig>,
pub http: Client,
/// Bot client for the Linear notification app.
pub lark_bot: Option<LarkBotClient>,
/// Bot client for the GitHub notification app.
/// Falls back to `lark_bot` when `None`.
pub github_lark_bot: Option<LarkBotClient>,
pub linear_client: Option<LinearClient>,
#[cfg(not(feature = "cf-worker"))]
pub update_debounce: DebounceMap,
Expand All @@ -278,15 +310,13 @@ impl AppState {
let github = GitHubConfig::from_env();

let http = Client::new();
let lark_bot = lark.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 – 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() {
Expand All @@ -305,6 +335,7 @@ impl AppState {
github,
http,
lark_bot,
github_lark_bot,
linear_client,
update_debounce: DebounceMap::new(),
}
Expand All @@ -320,7 +351,8 @@ impl AppState {
let github = GitHubConfig::from_worker_env(&env);

let http = Client::new();
let lark_bot = lark.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 {
Expand All @@ -330,6 +362,7 @@ impl AppState {
github,
http,
lark_bot,
github_lark_bot,
linear_client,
env,
}
Expand Down
61 changes: 21 additions & 40 deletions src/debounce_do.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
16 changes: 14 additions & 2 deletions src/dispatch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,24 @@

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;

if let (Some(email), Some(bot)) = (dm_email, &state.lark_bot) {
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;
}
}
43 changes: 23 additions & 20 deletions src/sinks/lark/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,32 +13,35 @@ use tracing::error;

use crate::{config::AppState, event::Event};

/// Sends a card notification for `event` to the Lark group.
///
/// Prefers Bot API (`target_chat_id`) when available, falls back to the
/// simple webhook (`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);
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");
}
}

match (&state.lark_bot, &state.lark.target_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}");
}
}
_ if !state.lark.webhook_url.is_empty() => {
webhook::send_lark_card(&state.http, &state.lark.webhook_url, &card).await;
}
_ => {
error!(
"no Lark delivery method configured (need LARK_TARGET_CHAT_ID + bot, or LARK_WEBHOOK_URL)"
);
}
/// Sends a card notification for `event` to the GitHub group chat via webhook.
///
/// 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 webhook = if !state.lark.github_webhook_url.is_empty() {
&state.lark.github_webhook_url
} else {
&state.lark.webhook_url
};
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
Expand Down
Loading