story-kit: merge 324_story_slack_bot_integration_for_bot_commands

This commit is contained in:
Dave
2026-03-20 01:09:55 +00:00
parent 4fe61c643b
commit 09890b5ea4
7 changed files with 1380 additions and 5 deletions

View File

@@ -29,6 +29,7 @@ use settings::SettingsApi;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use crate::slack::SlackWebhookContext;
use crate::whatsapp::WhatsAppWebhookContext;
const DEFAULT_PORT: u16 = 3001;
@@ -56,6 +57,7 @@ pub fn remove_port_file(path: &Path) {
pub fn build_routes(
ctx: AppContext,
whatsapp_ctx: Option<Arc<WhatsAppWebhookContext>>,
slack_ctx: Option<Arc<SlackWebhookContext>>,
) -> impl poem::Endpoint {
let ctx_arc = std::sync::Arc::new(ctx);
@@ -87,6 +89,13 @@ pub fn build_routes(
);
}
if let Some(sl_ctx) = slack_ctx {
route = route.at(
"/webhook/slack",
post(crate::slack::webhook_receive).data(sl_ctx),
);
}
route.data(ctx_arc)
}
@@ -196,6 +205,6 @@ mod tests {
fn build_routes_constructs_without_panic() {
let tmp = tempfile::tempdir().unwrap();
let ctx = context::AppContext::new_test(tmp.path().to_path_buf());
let _endpoint = build_routes(ctx, None);
let _endpoint = build_routes(ctx, None, None);
}
}

View File

@@ -14,6 +14,7 @@ mod state;
mod store;
pub mod transport;
mod workflow;
pub mod slack;
pub mod whatsapp;
mod worktree;
@@ -228,7 +229,38 @@ async fn main() -> Result<(), std::io::Error> {
})
});
let app = build_routes(ctx, whatsapp_ctx);
// Build Slack webhook context if bot.toml configures transport = "slack".
let slack_ctx: Option<Arc<slack::SlackWebhookContext>> = startup_root
.as_ref()
.and_then(|root| matrix::BotConfig::load(root))
.filter(|cfg| cfg.transport == "slack")
.map(|cfg| {
let transport = Arc::new(slack::SlackTransport::new(
cfg.slack_bot_token.clone().unwrap_or_default(),
));
let bot_name = cfg
.display_name
.clone()
.unwrap_or_else(|| "Assistant".to_string());
let root = startup_root.clone().unwrap();
let history = slack::load_slack_history(&root);
let channel_ids: std::collections::HashSet<String> =
cfg.slack_channel_ids.iter().cloned().collect();
Arc::new(slack::SlackWebhookContext {
signing_secret: cfg.slack_signing_secret.clone().unwrap_or_default(),
transport,
project_root: root,
agents: Arc::clone(&startup_agents),
bot_name,
bot_user_id: "slack-bot".to_string(),
ambient_rooms: Arc::new(std::sync::Mutex::new(std::collections::HashSet::new())),
history: std::sync::Arc::new(tokio::sync::Mutex::new(history)),
history_size: cfg.history_size,
channel_ids,
})
});
let app = build_routes(ctx, whatsapp_ctx, slack_ctx);
// Optional Matrix bot: connect to the homeserver and start listening for

View File

@@ -87,6 +87,19 @@ pub struct BotConfig {
/// use. Defaults to `"pipeline_notification"`.
#[serde(default)]
pub whatsapp_notification_template: Option<String>,
// ── Slack Bot API fields ─────────────────────────────────────────
// These are only required when `transport = "slack"`.
/// Slack Bot User OAuth Token (starts with `xoxb-`).
#[serde(default)]
pub slack_bot_token: Option<String>,
/// Slack Signing Secret used to verify incoming webhook requests.
#[serde(default)]
pub slack_signing_secret: Option<String>,
/// Slack channel IDs the bot should listen in.
#[serde(default)]
pub slack_channel_ids: Vec<String>,
}
fn default_transport() -> String {
@@ -142,6 +155,29 @@ impl BotConfig {
);
return None;
}
} else if config.transport == "slack" {
// Validate Slack-specific fields.
if config.slack_bot_token.as_ref().is_none_or(|s| s.is_empty()) {
eprintln!(
"[bot] bot.toml: transport=\"slack\" requires \
slack_bot_token"
);
return None;
}
if config.slack_signing_secret.as_ref().is_none_or(|s| s.is_empty()) {
eprintln!(
"[bot] bot.toml: transport=\"slack\" requires \
slack_signing_secret"
);
return None;
}
if config.slack_channel_ids.is_empty() {
eprintln!(
"[bot] bot.toml: transport=\"slack\" requires \
at least one slack_channel_ids entry"
);
return None;
}
} else if config.room_ids.is_empty() {
eprintln!(
"[matrix-bot] bot.toml has no room_ids configured — \
@@ -680,6 +716,97 @@ enabled = true
transport = "whatsapp"
whatsapp_phone_number_id = "123456"
whatsapp_access_token = "EAAtoken"
"#,
)
.unwrap();
assert!(BotConfig::load(tmp.path()).is_none());
}
// ── Slack config tests ─────────────────────────────────────────────
#[test]
fn load_slack_transport_reads_config() {
let tmp = tempfile::tempdir().unwrap();
let sk = tmp.path().join(".story_kit");
fs::create_dir_all(&sk).unwrap();
fs::write(
sk.join("bot.toml"),
r#"
homeserver = "https://matrix.example.com"
username = "@bot:example.com"
password = "secret"
enabled = true
transport = "slack"
slack_bot_token = "xoxb-123"
slack_signing_secret = "secret123"
slack_channel_ids = ["C01ABCDEF"]
"#,
)
.unwrap();
let config = BotConfig::load(tmp.path()).unwrap();
assert_eq!(config.transport, "slack");
assert_eq!(config.slack_bot_token.as_deref(), Some("xoxb-123"));
assert_eq!(config.slack_signing_secret.as_deref(), Some("secret123"));
assert_eq!(config.slack_channel_ids, vec!["C01ABCDEF"]);
}
#[test]
fn load_slack_returns_none_when_missing_bot_token() {
let tmp = tempfile::tempdir().unwrap();
let sk = tmp.path().join(".story_kit");
fs::create_dir_all(&sk).unwrap();
fs::write(
sk.join("bot.toml"),
r#"
homeserver = "https://matrix.example.com"
username = "@bot:example.com"
password = "secret"
enabled = true
transport = "slack"
slack_signing_secret = "secret123"
slack_channel_ids = ["C01ABCDEF"]
"#,
)
.unwrap();
assert!(BotConfig::load(tmp.path()).is_none());
}
#[test]
fn load_slack_returns_none_when_missing_signing_secret() {
let tmp = tempfile::tempdir().unwrap();
let sk = tmp.path().join(".story_kit");
fs::create_dir_all(&sk).unwrap();
fs::write(
sk.join("bot.toml"),
r#"
homeserver = "https://matrix.example.com"
username = "@bot:example.com"
password = "secret"
enabled = true
transport = "slack"
slack_bot_token = "xoxb-123"
slack_channel_ids = ["C01ABCDEF"]
"#,
)
.unwrap();
assert!(BotConfig::load(tmp.path()).is_none());
}
#[test]
fn load_slack_returns_none_when_missing_channel_ids() {
let tmp = tempfile::tempdir().unwrap();
let sk = tmp.path().join(".story_kit");
fs::create_dir_all(&sk).unwrap();
fs::write(
sk.join("bot.toml"),
r#"
homeserver = "https://matrix.example.com"
username = "@bot:example.com"
password = "secret"
enabled = true
transport = "slack"
slack_bot_token = "xoxb-123"
slack_signing_secret = "secret123"
"#,
)
.unwrap();

View File

@@ -62,9 +62,12 @@ pub fn spawn_bot(
}
};
// WhatsApp transport is handled via HTTP webhooks, not the Matrix sync loop.
if config.transport == "whatsapp" {
crate::slog!("[bot] transport=whatsapp — skipping Matrix bot; webhooks handle WhatsApp");
// WhatsApp and Slack transports are handled via HTTP webhooks, not the Matrix sync loop.
if config.transport == "whatsapp" || config.transport == "slack" {
crate::slog!(
"[bot] transport={} — skipping Matrix bot; webhooks handle this transport",
config.transport
);
return;
}

1180
server/src/slack.rs Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -83,4 +83,15 @@ mod tests {
fn assert_send_sync<T: Send + Sync>() {}
assert_send_sync::<crate::matrix::transport_impl::MatrixTransport>();
}
/// Verify that SlackTransport satisfies the ChatTransport trait and
/// can be used as `Arc<dyn ChatTransport>` (compile-time check).
#[test]
fn slack_transport_satisfies_trait() {
fn assert_transport<T: ChatTransport>() {}
assert_transport::<crate::slack::SlackTransport>();
let _: Arc<dyn ChatTransport> =
Arc::new(crate::slack::SlackTransport::new("xoxb-test".to_string()));
}
}