diff --git a/server/src/config.rs b/server/src/config.rs index 39ef3d79..275821d5 100644 --- a/server/src/config.rs +++ b/server/src/config.rs @@ -63,6 +63,13 @@ pub struct ProjectConfig { /// Default: `true`. #[serde(default = "default_slack_status_consumer")] pub slack_status_consumer: bool, + /// Whether the Discord bot subscribes to the status broadcaster and forwards + /// pipeline events to its configured channels. + /// Set to `false` to silence Discord status notifications without affecting + /// other consumers (web UI, Matrix, Slack, WhatsApp, agent context). + /// Default: `true`. + #[serde(default = "default_discord_status_consumer")] + pub discord_status_consumer: bool, /// IANA timezone name (e.g. `"Europe/London"`, `"America/New_York"`). /// When set, timer HH:MM inputs are interpreted in this timezone instead /// of the container/host local time. Falls back to `chrono::Local` when absent. @@ -155,6 +162,10 @@ fn default_slack_status_consumer() -> bool { true } +fn default_discord_status_consumer() -> bool { + true +} + fn default_max_mesh_peers() -> usize { 3 } @@ -287,6 +298,7 @@ impl Default for ProjectConfig { web_ui_status_consumer: default_web_ui_status_consumer(), matrix_status_consumer: default_matrix_status_consumer(), slack_status_consumer: default_slack_status_consumer(), + discord_status_consumer: default_discord_status_consumer(), timezone: None, rendezvous: None, trusted_keys: Vec::new(), @@ -371,6 +383,7 @@ impl ProjectConfig { web_ui_status_consumer: default_web_ui_status_consumer(), matrix_status_consumer: default_matrix_status_consumer(), slack_status_consumer: default_slack_status_consumer(), + discord_status_consumer: default_discord_status_consumer(), timezone: legacy.timezone, rendezvous: None, trusted_keys: Vec::new(), @@ -406,6 +419,7 @@ impl ProjectConfig { web_ui_status_consumer: default_web_ui_status_consumer(), matrix_status_consumer: default_matrix_status_consumer(), slack_status_consumer: default_slack_status_consumer(), + discord_status_consumer: default_discord_status_consumer(), timezone: legacy.timezone, rendezvous: None, trusted_keys: Vec::new(), @@ -429,6 +443,7 @@ impl ProjectConfig { web_ui_status_consumer: default_web_ui_status_consumer(), matrix_status_consumer: default_matrix_status_consumer(), slack_status_consumer: default_slack_status_consumer(), + discord_status_consumer: default_discord_status_consumer(), timezone: legacy.timezone, rendezvous: None, trusted_keys: Vec::new(), diff --git a/server/src/main.rs b/server/src/main.rs index f79d53b1..266df7c7 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -809,6 +809,37 @@ async fn main() -> Result<(), std::io::Error> { watcher_rx_for_discord, root.clone(), ); + + // Subscribe to the status broadcaster if the discord_status_consumer toggle + // is enabled (default: true). Formats each StatusEvent via the common + // formatter and sends the resulting text to all configured Discord channels. + // The task exits automatically when the broadcaster is dropped on shutdown. + { + use crate::service::status::format::format_status_event; + + let status_enabled = config::ProjectConfig::load(root) + .map(|c| c.discord_status_consumer) + .unwrap_or(true); + + if status_enabled { + let mut sub = ctx.services.status.subscribe(); + let transport = Arc::clone(&ctx.transport) as Arc; + let channels: Vec = ctx.channel_ids.iter().cloned().collect(); + tokio::spawn(async move { + while let Some(event) = sub.recv().await { + let plain = format_status_event(&event); + for channel in &channels { + if let Err(e) = transport.send_message(channel, &plain, "").await { + crate::slog!( + "[discord] Failed to send status event to {channel}: {e}" + ); + } + } + } + crate::slog!("[discord] Status subscriber task exiting — broadcaster dropped"); + }); + } + } } else { drop(watcher_rx_for_discord); } diff --git a/server/src/worktree.rs b/server/src/worktree.rs index 6374c941..a7efd762 100644 --- a/server/src/worktree.rs +++ b/server/src/worktree.rs @@ -531,6 +531,7 @@ mod tests { web_ui_status_consumer: true, matrix_status_consumer: true, slack_status_consumer: true, + discord_status_consumer: true, timezone: None, rendezvous: None, trusted_keys: Vec::new(), @@ -563,6 +564,7 @@ mod tests { web_ui_status_consumer: true, matrix_status_consumer: true, slack_status_consumer: true, + discord_status_consumer: true, timezone: None, rendezvous: None, trusted_keys: Vec::new(), @@ -595,6 +597,7 @@ mod tests { web_ui_status_consumer: true, matrix_status_consumer: true, slack_status_consumer: true, + discord_status_consumer: true, timezone: None, rendezvous: None, trusted_keys: Vec::new(), @@ -627,6 +630,7 @@ mod tests { web_ui_status_consumer: true, matrix_status_consumer: true, slack_status_consumer: true, + discord_status_consumer: true, timezone: None, rendezvous: None, trusted_keys: Vec::new(), @@ -658,6 +662,7 @@ mod tests { web_ui_status_consumer: true, matrix_status_consumer: true, slack_status_consumer: true, + discord_status_consumer: true, timezone: None, rendezvous: None, trusted_keys: Vec::new(), @@ -696,6 +701,7 @@ mod tests { web_ui_status_consumer: true, matrix_status_consumer: true, slack_status_consumer: true, + discord_status_consumer: true, timezone: None, rendezvous: None, trusted_keys: Vec::new(), @@ -775,6 +781,7 @@ mod tests { web_ui_status_consumer: true, matrix_status_consumer: true, slack_status_consumer: true, + discord_status_consumer: true, timezone: None, rendezvous: None, trusted_keys: Vec::new(), @@ -812,6 +819,7 @@ mod tests { web_ui_status_consumer: true, matrix_status_consumer: true, slack_status_consumer: true, + discord_status_consumer: true, timezone: None, rendezvous: None, trusted_keys: Vec::new(), @@ -896,6 +904,7 @@ mod tests { web_ui_status_consumer: true, matrix_status_consumer: true, slack_status_consumer: true, + discord_status_consumer: true, timezone: None, rendezvous: None, trusted_keys: Vec::new(), @@ -936,6 +945,7 @@ mod tests { web_ui_status_consumer: true, matrix_status_consumer: true, slack_status_consumer: true, + discord_status_consumer: true, timezone: None, rendezvous: None, trusted_keys: Vec::new(), @@ -966,6 +976,7 @@ mod tests { web_ui_status_consumer: true, matrix_status_consumer: true, slack_status_consumer: true, + discord_status_consumer: true, timezone: None, rendezvous: None, trusted_keys: Vec::new(), @@ -1002,6 +1013,7 @@ mod tests { web_ui_status_consumer: true, matrix_status_consumer: true, slack_status_consumer: true, + discord_status_consumer: true, timezone: None, rendezvous: None, trusted_keys: Vec::new(),