huskies: merge 599_story_cross_project_status_notifications_in_chat

This commit is contained in:
dave
2026-04-23 12:05:27 +00:00
parent 4b765bbc39
commit 3521649cbf
8 changed files with 1080 additions and 3 deletions
@@ -168,6 +168,11 @@ pub async fn run_bot(
let notif_room_ids = target_room_ids.clone();
let notif_project_root = project_root.clone();
let announce_room_ids = target_room_ids.clone();
// Clone values needed by the gateway notification poller (only used in gateway mode).
let poller_room_ids: Vec<String> = target_room_ids.iter().map(|r| r.to_string()).collect();
let poller_project_urls = gateway_project_urls.clone();
let poller_poll_interval = config.aggregated_notifications_poll_interval_secs;
let poller_enabled = config.aggregated_notifications_enabled;
let persisted = load_history(&project_root);
slog!(
@@ -271,6 +276,20 @@ pub async fn run_bot(
notif_project_root,
);
// In gateway mode, spawn the cross-project notification poller.
// It polls every registered project's `/api/events` endpoint and forwards
// new events to the configured gateway rooms with a `[project-name]` prefix.
// The poller is controlled by the gateway-level `aggregated_notifications_enabled`
// flag in bot.toml — set it to `false` to disable without touching per-project configs.
if !poller_project_urls.is_empty() && poller_enabled {
crate::gateway::spawn_gateway_notification_poller(
Arc::clone(&transport),
poller_room_ids,
poller_project_urls,
poller_poll_interval,
);
}
// Spawn a shutdown watcher that sends a best-effort goodbye message to all
// configured rooms when the server is about to stop (SIGINT/SIGTERM or rebuild).
{
@@ -10,6 +10,14 @@ fn default_permission_timeout_secs() -> u64 {
120
}
fn default_aggregated_notifications_poll_interval_secs() -> u64 {
5
}
fn default_aggregated_notifications_enabled() -> bool {
true
}
/// Configuration for the Matrix bot, read from `.huskies/bot.toml`.
#[derive(Deserialize, Clone, Debug)]
pub struct BotConfig {
@@ -146,6 +154,26 @@ pub struct BotConfig {
/// When empty or absent, all users in configured channels are allowed.
#[serde(default)]
pub discord_allowed_users: Vec<String>,
/// How often (in seconds) the gateway polls each project server's
/// `/api/events` endpoint to aggregate cross-project notifications.
///
/// Only used when the gateway's bot is enabled. Defaults to 5 seconds.
#[serde(default = "default_aggregated_notifications_poll_interval_secs")]
pub aggregated_notifications_poll_interval_secs: u64,
/// Whether the gateway-level aggregated cross-project notification stream
/// is enabled. When `false`, the gateway will not poll per-project
/// servers for events even if the bot is otherwise enabled.
///
/// Set this in the **gateway's** `bot.toml` (not in per-project configs).
/// Adding a new project to `projects.toml` never requires touching
/// per-project bot configs — the aggregated stream picks it up
/// automatically once this flag is `true` (the default).
///
/// Defaults to `true`.
#[serde(default = "default_aggregated_notifications_enabled")]
pub aggregated_notifications_enabled: bool,
}
fn default_transport() -> String {
@@ -658,6 +686,47 @@ require_verified_devices = true
);
}
#[test]
fn aggregated_notifications_enabled_defaults_to_true() {
let tmp = tempfile::tempdir().unwrap();
let sk = tmp.path().join(".huskies");
fs::create_dir_all(&sk).unwrap();
fs::write(
sk.join("bot.toml"),
r#"
homeserver = "https://matrix.example.com"
username = "@bot:example.com"
password = "secret"
room_ids = ["!abc:example.com"]
enabled = true
"#,
)
.unwrap();
let config = BotConfig::load(tmp.path()).unwrap();
assert!(config.aggregated_notifications_enabled);
}
#[test]
fn aggregated_notifications_enabled_can_be_set_to_false() {
let tmp = tempfile::tempdir().unwrap();
let sk = tmp.path().join(".huskies");
fs::create_dir_all(&sk).unwrap();
fs::write(
sk.join("bot.toml"),
r#"
homeserver = "https://matrix.example.com"
username = "@bot:example.com"
password = "secret"
room_ids = ["!abc:example.com"]
enabled = true
aggregated_notifications_enabled = false
"#,
)
.unwrap();
let config = BotConfig::load(tmp.path()).unwrap();
assert!(!config.aggregated_notifications_enabled);
}
#[test]
fn load_reads_ambient_rooms() {
let tmp = tempfile::tempdir().unwrap();