use super::error::Result;
use super::r#trait::{Tool, ToolCapability, ToolExecutionContext, ToolResult};
use crate::channels::ChannelFactory;
use crate::channels::slack::SlackState;
use async_trait::async_trait;
use serde_json::Value;
use std::sync::Arc;
use std::time::Duration;
pub struct SlackConnectTool {
channel_factory: Arc<ChannelFactory>,
slack_state: Arc<SlackState>,
}
impl SlackConnectTool {
pub fn new(channel_factory: Arc<ChannelFactory>, slack_state: Arc<SlackState>) -> Self {
Self {
channel_factory,
slack_state,
}
}
}
#[async_trait]
impl Tool for SlackConnectTool {
fn name(&self) -> &str {
"slack_connect"
}
fn description(&self) -> &str {
"Connect a Slack bot to OpenCrabs via Socket Mode. Requires two tokens: \
a Bot Token (xoxb-...) and an App-Level Token (xapp-...). \
The user must create an app at https://api.slack.com/apps, enable Socket Mode, \
add an App-Level Token with 'connections:write' scope, and install the app to their workspace. \
Call this when the user asks to connect or set up Slack."
}
fn input_schema(&self) -> Value {
serde_json::json!({
"type": "object",
"properties": {
"bot_token": {
"type": "string",
"description": "Slack Bot Token (starts with xoxb-)"
},
"app_token": {
"type": "string",
"description": "Slack App-Level Token (starts with xapp-). Required for Socket Mode."
},
"channel_id": {
"type": "string",
"description": "Slack channel ID where the bot should send its welcome message \
(e.g. 'C12345678'). Right-click a channel → View channel details → \
copy the Channel ID at the bottom."
},
"allowed_users": {
"type": "array",
"items": { "type": "string" },
"description": "Slack user IDs allowed to talk to the bot (e.g. 'U12345678'). \
Ask the user for their Slack member ID (Profile → ⋯ → Copy member ID). \
If empty, all workspace users can message the bot."
}
},
"required": ["bot_token", "app_token", "allowed_users"]
})
}
fn capabilities(&self) -> Vec<ToolCapability> {
vec![ToolCapability::Network, ToolCapability::SystemModification]
}
async fn execute(&self, input: Value, _context: &ToolExecutionContext) -> Result<ToolResult> {
if self.slack_state.is_connected().await {
return Ok(ToolResult::success(
"Slack is already connected.".to_string(),
));
}
let bot_token = match input.get("bot_token").and_then(|v| v.as_str()) {
Some(t) if !t.is_empty() => t.to_string(),
_ => {
return Ok(ToolResult::error(
"Missing or empty 'bot_token' parameter. \
The user needs their Slack Bot Token (starts with xoxb-)."
.to_string(),
));
}
};
let app_token = match input.get("app_token").and_then(|v| v.as_str()) {
Some(t) if !t.is_empty() => t.to_string(),
_ => {
return Ok(ToolResult::error(
"Missing or empty 'app_token' parameter. \
The user needs their Slack App-Level Token (starts with xapp-). \
This is required for Socket Mode."
.to_string(),
));
}
};
let allowed_users: Vec<String> = input
.get("allowed_users")
.and_then(|v| serde_json::from_value(v.clone()).ok())
.unwrap_or_default();
if let Err(e) = crate::config::write_secret_key("channels.slack", "token", &bot_token) {
tracing::error!("Failed to save Slack bot token: {}", e);
}
if let Err(e) = crate::config::write_secret_key("channels.slack", "app_token", &app_token) {
tracing::error!("Failed to save Slack app token: {}", e);
}
if let Err(e) = crate::config::Config::write_key("channels.slack", "enabled", "true") {
tracing::error!("Failed to enable Slack in config: {}", e);
}
if let Err(e) = crate::config::Config::write_key("channels.slack", "token", &bot_token) {
tracing::error!("Failed to save Slack token to config: {}", e);
}
if let Err(e) = crate::config::Config::write_key("channels.slack", "app_token", &app_token)
{
tracing::error!("Failed to save Slack app_token to config: {}", e);
}
if !allowed_users.is_empty()
&& let Err(e) = crate::config::Config::write_array(
"channels.slack",
"allowed_users",
&allowed_users,
)
{
tracing::error!("Failed to save Slack allowed_users: {}", e);
}
let factory = self.channel_factory.clone();
let agent = factory.create_agent_service().await;
let service_context = factory.service_context();
let shared_session = factory.shared_session_id();
let slack_state = self.slack_state.clone();
let config_rx = factory.config_rx();
let channel_msg_repo =
crate::db::ChannelMessageRepository::new(factory.service_context().pool());
let sl_agent = crate::channels::slack::SlackAgent::new(
agent,
service_context,
shared_session,
slack_state.clone(),
config_rx,
channel_msg_repo,
);
let _handle = sl_agent.start(bot_token, app_token);
let timeout = Duration::from_secs(30);
let start = std::time::Instant::now();
loop {
if slack_state.is_connected().await {
if let Some(ch) = input
.get("channel_id")
.and_then(|v| v.as_str())
.filter(|s| !s.is_empty())
{
slack_state.set_owner_channel(ch.to_string()).await;
}
let mut msg = "Slack bot connected successfully via Socket Mode! \
Now listening for messages. Connection persists across restarts."
.to_string();
if input
.get("channel_id")
.and_then(|v| v.as_str())
.is_some_and(|s| !s.is_empty())
{
msg.push_str(
"\n\nIMPORTANT: Send a welcome message to the user RIGHT NOW \
using the `slack_send` tool. Be wildly fun — talk like their \
best buddy from childhood. You can curse, be unhinged, hilarious, \
and totally informal. Use your personality, SOUL.md, and any context \
you have about the user. Make it memorable and uniquely yours. \
No generic corporate greetings — surprise them!",
);
}
return Ok(ToolResult::success(msg));
}
if start.elapsed() > timeout {
return Ok(ToolResult::error(
"Timed out waiting for Slack bot to connect (30s). \
Check that both tokens are valid. The Bot Token should start with 'xoxb-' \
and the App Token with 'xapp-'. Socket Mode must be enabled in your Slack app settings."
.to_string(),
));
}
tokio::time::sleep(Duration::from_millis(250)).await;
}
}
}