use super::error::Result;
use super::r#trait::{Tool, ToolCapability, ToolExecutionContext, ToolResult};
use crate::channels::ChannelFactory;
use crate::channels::trello::TrelloState;
use crate::channels::trello::client::TrelloClient;
use async_trait::async_trait;
use serde_json::Value;
use std::sync::Arc;
use std::time::Duration;
pub struct TrelloConnectTool {
channel_factory: Arc<ChannelFactory>,
trello_state: Arc<TrelloState>,
}
impl TrelloConnectTool {
pub fn new(channel_factory: Arc<ChannelFactory>, trello_state: Arc<TrelloState>) -> Self {
Self {
channel_factory,
trello_state,
}
}
}
#[async_trait]
impl Tool for TrelloConnectTool {
fn name(&self) -> &str {
"trello_connect"
}
fn description(&self) -> &str {
"Connect one or more Trello boards to OpenCrabs. Accepts a Trello API Key and API Token \
from https://trello.com/power-ups/admin, plus board names or IDs to monitor. \
Board names are resolved automatically — you can mix names and 24-char hex IDs. \
Once connected, the agent polls every 30 s for new card comments and replies. \
Call this when the user asks to connect or set up Trello."
}
fn input_schema(&self) -> Value {
serde_json::json!({
"type": "object",
"properties": {
"api_key": {
"type": "string",
"description": "Trello API Key from https://trello.com/power-ups/admin"
},
"api_token": {
"type": "string",
"description": "Trello API Token (generated alongside the API Key)"
},
"boards": {
"type": "array",
"items": { "type": "string" },
"description": "Board names or IDs to monitor for card comments. \
Names are resolved to IDs automatically. \
At least one board is required."
},
"allowed_users": {
"type": "array",
"items": { "type": "string" },
"description": "Trello member IDs allowed to interact with the bot. \
Empty = respond to all members."
}
},
"required": ["api_key", "api_token", "boards"]
})
}
fn capabilities(&self) -> Vec<ToolCapability> {
vec![ToolCapability::Network, ToolCapability::SystemModification]
}
async fn execute(&self, input: Value, _context: &ToolExecutionContext) -> Result<ToolResult> {
if self.trello_state.is_connected().await {
return Ok(ToolResult::success(
"Trello is already connected and polling.".to_string(),
));
}
let api_key = match input.get("api_key").and_then(|v| v.as_str()) {
Some(k) if !k.is_empty() => k.to_string(),
_ => {
return Ok(ToolResult::error(
"Missing or empty 'api_key'. Get it from https://trello.com/power-ups/admin"
.to_string(),
));
}
};
let api_token = match input.get("api_token").and_then(|v| v.as_str()) {
Some(t) if !t.is_empty() => t.to_string(),
_ => {
return Ok(ToolResult::error(
"Missing or empty 'api_token'. Get it from https://trello.com/power-ups/admin"
.to_string(),
));
}
};
let board_queries: Vec<String> = input
.get("boards")
.and_then(|v| serde_json::from_value::<Vec<String>>(v.clone()).ok())
.unwrap_or_default()
.into_iter()
.filter(|s| !s.is_empty())
.collect();
if board_queries.is_empty() {
return Ok(ToolResult::error(
"Missing or empty 'boards'. Provide at least one board name or ID.".to_string(),
));
}
let allowed_users: Vec<String> = input
.get("allowed_users")
.and_then(|v| serde_json::from_value::<Vec<String>>(v.clone()).ok())
.unwrap_or_default();
let client = TrelloClient::new(&api_key, &api_token);
let me = match client.get_member_me().await {
Ok(m) => m,
Err(e) => {
return Ok(ToolResult::error(format!(
"Trello credential verification failed: {}. \
Check your API Key and Token at https://trello.com/power-ups/admin",
e
)));
}
};
let mut board_ids: Vec<String> = Vec::new();
let mut board_names: Vec<String> = Vec::new();
for query in &board_queries {
match client.resolve_board(query).await {
Ok(id) => {
board_names.push(query.clone());
board_ids.push(id);
}
Err(e) => {
return Ok(ToolResult::error(format!(
"Could not find Trello board '{}': {}",
query, e
)));
}
}
}
let mut total_open_cards = 0usize;
for bid in &board_ids {
total_open_cards += client
.get_board_cards(bid)
.await
.map(|c| c.len())
.unwrap_or(0);
}
if let Err(e) = crate::config::write_secret_key("channels.trello", "app_token", &api_key) {
tracing::error!("Failed to save Trello API key: {}", e);
}
if let Err(e) = crate::config::write_secret_key("channels.trello", "token", &api_token) {
tracing::error!("Failed to save Trello API token: {}", e);
}
if let Err(e) = crate::config::Config::write_key("channels.trello", "enabled", "true") {
tracing::error!("Failed to enable Trello in config: {}", e);
}
if let Err(e) =
crate::config::Config::write_array("channels.trello", "board_ids", &board_ids)
{
tracing::error!("Failed to save Trello board_ids: {}", e);
}
if !allowed_users.is_empty()
&& let Err(e) = crate::config::Config::write_array(
"channels.trello",
"allowed_users",
&allowed_users,
)
{
tracing::error!("Failed to save Trello allowed_users: {}", e);
}
let factory = self.channel_factory.clone();
let agent_svc = factory.create_agent_service().await;
let service_ctx = factory.service_context();
let shared_session = factory.shared_session_id();
let trello_state = self.trello_state.clone();
let idle_timeout_hours = crate::config::Config::load()
.ok()
.and_then(|c| c.channels.trello.session_idle_hours);
let trello_agent = crate::channels::trello::TrelloAgent::new(
agent_svc,
service_ctx,
allowed_users,
shared_session,
trello_state.clone(),
board_ids,
None, idle_timeout_hours,
);
let _handle = trello_agent.start(api_key, api_token);
let timeout = Duration::from_secs(5);
let start = std::time::Instant::now();
loop {
if trello_state.is_connected().await {
break;
}
if start.elapsed() >= timeout {
break;
}
tokio::time::sleep(Duration::from_millis(250)).await;
}
let connected = trello_state.is_connected().await;
if connected {
Ok(ToolResult::success(format!(
"Trello connected! Monitoring {} board(s): {} ({} total open cards).\n\
Authenticated as @{}. Polling every 30 seconds for new card comments.",
board_names.len(),
board_names.join(", "),
total_open_cards,
me.username
)))
} else {
Ok(ToolResult::error(
"Trello agent started but did not confirm connection within 5 seconds. \
Check the logs for more detail."
.to_string(),
))
}
}
}