use super::TrelloState;
use super::client::TrelloClient;
use super::handler;
use crate::brain::agent::AgentService;
use crate::services::{ServiceContext, SessionService};
use std::sync::Arc;
use tokio::sync::Mutex;
use uuid::Uuid;
pub struct TrelloAgent {
agent_service: Arc<AgentService>,
service_context: ServiceContext,
allowed_users: Vec<String>,
shared_session_id: Arc<Mutex<Option<Uuid>>>,
trello_state: Arc<TrelloState>,
board_ids: Vec<String>,
poll_interval_secs: Option<u64>,
idle_timeout_hours: Option<f64>,
}
impl TrelloAgent {
#[allow(clippy::too_many_arguments)]
pub fn new(
agent_service: Arc<AgentService>,
service_context: ServiceContext,
allowed_users: Vec<String>,
shared_session_id: Arc<Mutex<Option<Uuid>>>,
trello_state: Arc<TrelloState>,
board_ids: Vec<String>,
poll_interval_secs: Option<u64>,
idle_timeout_hours: Option<f64>,
) -> Self {
Self {
agent_service,
service_context,
allowed_users,
shared_session_id,
trello_state,
board_ids,
poll_interval_secs,
idle_timeout_hours,
}
}
pub fn start(self, api_key: String, api_token: String) -> tokio::task::JoinHandle<()> {
tokio::spawn(async move {
if api_key.is_empty() || api_token.is_empty() {
tracing::warn!("Trello: credentials missing, not starting agent");
return;
}
let client = TrelloClient::new(&api_key, &api_token);
let bot_member = match client.get_member_me().await {
Ok(m) => m,
Err(e) => {
tracing::error!("Trello: credential verification failed: {}", e);
return;
}
};
self.trello_state
.set_bot_member_id(bot_member.id.clone())
.await;
self.trello_state.set_credentials(api_key, api_token).await;
self.trello_state.set_connected(true).await;
let interval_secs = self.poll_interval_secs.unwrap_or(0);
if interval_secs == 0 {
tracing::info!(
"Trello: authenticated as @{} — tool-only mode (no polling). \
Set poll_interval_secs in config to enable @mention polling.",
bot_member.username,
);
return;
}
tracing::info!(
"Trello: authenticated as @{} ({}), polling {} board(s) every {}s for @mentions",
bot_member.username,
bot_member.id,
self.board_ids.len(),
interval_secs,
);
let session_svc = SessionService::new(self.service_context.clone());
let idle_timeout_hours = self.idle_timeout_hours;
let owner_member_id = self.allowed_users.first().cloned();
let mut last_checked = chrono::Utc::now().to_rfc3339();
loop {
tokio::time::sleep(std::time::Duration::from_secs(interval_secs)).await;
let tick_time = chrono::Utc::now().to_rfc3339();
for board_id in &self.board_ids {
let actions = match client
.get_board_actions_since(board_id, &last_checked)
.await
{
Ok(a) => a,
Err(e) => {
tracing::warn!(
"Trello: failed to fetch actions for board {}: {}",
board_id,
e
);
continue;
}
};
for action in &actions {
if action.action_type != "commentCard" {
continue;
}
if action.id_member_creator == bot_member.id {
continue;
}
let mention = format!("@{}", bot_member.username);
if !action.data.text.contains(&mention) {
tracing::debug!("Trello: skipping comment — bot not @mentioned");
continue;
}
if !self.allowed_users.is_empty()
&& !self.allowed_users.contains(&action.id_member_creator)
{
tracing::debug!(
"Trello: skipping comment from non-allowed member {}",
action.id_member_creator
);
continue;
}
handler::process_comment(
action,
&client,
self.agent_service.clone(),
session_svc.clone(),
self.shared_session_id.clone(),
owner_member_id.as_deref(),
idle_timeout_hours,
)
.await;
}
}
last_checked = tick_time;
}
})
}
}