use axum::extract::{Path, Query, State}; use axum::http::StatusCode; use axum::Json; use serde::{Deserialize, Serialize}; use time::OffsetDateTime; use crate::db; use crate::state::AppState; #[derive(Deserialize)] pub struct UserQuery { pub user_id: String, } fn format_time(t: OffsetDateTime) -> String { t.format(&time::format_description::well_known::Rfc3339) .unwrap_or_else(|_| t.to_string()) } // --- Bot types --- #[derive(Serialize)] pub struct BotResponse { pub id: String, pub name: String, pub nick: String, pub network_addr: String, pub sasl_username: Option, pub channels: Vec, pub enabled: bool, pub status: String, pub status_message: Option, pub running: bool, pub created_at: String, pub updated_at: String, } #[derive(Deserialize)] pub struct CreateBotRequest { pub user_id: String, pub plan: String, pub name: String, pub nick: String, pub network_addr: String, pub sasl_username: Option, pub sasl_password: Option, pub channels: Vec, } #[derive(Deserialize)] pub struct UpdateBotRequest { pub user_id: String, pub name: String, pub nick: String, pub network_addr: String, pub sasl_username: Option, pub sasl_password: Option, pub channels: Vec, } // --- Script types --- #[derive(Serialize)] pub struct ScriptResponse { pub id: String, pub bot_id: String, pub name: String, pub source: String, pub enabled: bool, pub created_at: String, pub updated_at: String, } #[derive(Deserialize)] pub struct CreateScriptRequest { pub user_id: String, pub name: String, pub source: String, } #[derive(Deserialize)] pub struct UpdateScriptRequest { pub user_id: String, pub name: String, pub source: String, pub enabled: bool, } // --- Log types --- #[derive(Serialize)] pub struct LogResponse { pub id: i64, pub level: String, pub message: String, pub created_at: String, } // --- KV types --- #[derive(Serialize)] pub struct KvResponse { pub key: String, pub value: String, pub updated_at: String, } #[derive(Deserialize)] pub struct SetKvRequest { pub user_id: String, pub value: String, } // --- Usage types --- #[derive(Serialize)] pub struct UsageResponse { pub bot_count: i64, pub bots_enabled: i64, pub bots_running: i64, } // --- Error helper --- #[derive(Serialize)] pub struct ErrorBody { error: String, } fn error_json(status: StatusCode, msg: &str) -> (StatusCode, Json) { ( status, Json(ErrorBody { error: msg.to_string(), }), ) } // --- Conversions --- async fn bot_to_response(state: &AppState, bot: db::Bot) -> BotResponse { let running = state.bot_manager.is_running(&bot.id).await; BotResponse { id: bot.id, name: bot.name, nick: bot.nick, network_addr: bot.network_addr, sasl_username: bot.sasl_username, channels: bot.channels, enabled: bot.enabled, status: bot.status, status_message: bot.status_message, running, created_at: format_time(bot.created_at), updated_at: format_time(bot.updated_at), } } fn script_to_response(s: db::BotScript) -> ScriptResponse { ScriptResponse { id: s.id, bot_id: s.bot_id, name: s.name, source: s.source, enabled: s.enabled, created_at: format_time(s.created_at), updated_at: format_time(s.updated_at), } } fn log_to_response(l: db::BotLog) -> LogResponse { LogResponse { id: l.id, level: l.level, message: l.message, created_at: format_time(l.created_at), } } fn kv_to_response(kv: db::BotKv) -> KvResponse { KvResponse { key: kv.key, value: kv.value, updated_at: format_time(kv.updated_at), } } // --- Bot endpoints --- pub async fn list_bots( State(state): State, Query(q): Query, ) -> Result>, (StatusCode, Json)> { let bots = db::list_bots(&state.db, &q.user_id) .await .map_err(|e| error_json(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()))?; let mut out = Vec::with_capacity(bots.len()); for bot in bots { out.push(bot_to_response(&state, bot).await); } Ok(Json(out)) } pub async fn create_bot( State(state): State, Json(req): Json, ) -> Result<(StatusCode, Json), (StatusCode, Json)> { let limit: i64 = match req.plan.as_str() { "pro" => 5, _ => 1, }; let count = db::count_bots(&state.db, &req.user_id) .await .map_err(|e| error_json(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()))?; if count >= limit { return Err(error_json( StatusCode::FORBIDDEN, &format!("bot limit reached ({limit})"), )); } let id = nanoid::nanoid!(12); db::insert_bot( &state.db, &id, &req.user_id, &req.name, &req.nick, &req.network_addr, req.sasl_username.as_deref(), req.sasl_password.as_deref(), &req.channels, ) .await .map_err(|e| error_json(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()))?; let bot = db::get_bot(&state.db, &id) .await .map_err(|e| error_json(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()))? .ok_or_else(|| error_json(StatusCode::INTERNAL_SERVER_ERROR, "bot not found after insert"))?; let resp = bot_to_response(&state, bot).await; Ok((StatusCode::CREATED, Json(resp))) } pub async fn get_bot( State(state): State, Path(id): Path, Query(q): Query, ) -> Result, (StatusCode, Json)> { let bot = db::get_bot_owned(&state.db, &id, &q.user_id) .await .map_err(|e| error_json(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()))? .ok_or_else(|| error_json(StatusCode::NOT_FOUND, "bot not found"))?; Ok(Json(bot_to_response(&state, bot).await)) } pub async fn update_bot( State(state): State, Path(id): Path, Json(req): Json, ) -> Result, (StatusCode, Json)> { let updated = db::update_bot( &state.db, &id, &req.user_id, &req.name, &req.nick, &req.network_addr, req.sasl_username.as_deref(), req.sasl_password.as_deref(), &req.channels, ) .await .map_err(|e| error_json(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()))?; if !updated { return Err(error_json(StatusCode::NOT_FOUND, "bot not found")); } let bot = db::get_bot_owned(&state.db, &id, &req.user_id) .await .map_err(|e| error_json(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()))? .ok_or_else(|| error_json(StatusCode::NOT_FOUND, "bot not found"))?; Ok(Json(bot_to_response(&state, bot).await)) } pub async fn delete_bot( State(state): State, Path(id): Path, Query(q): Query, ) -> Result)> { let _bot = db::get_bot_owned(&state.db, &id, &q.user_id) .await .map_err(|e| error_json(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()))? .ok_or_else(|| error_json(StatusCode::NOT_FOUND, "bot not found"))?; if state.bot_manager.is_running(&id).await { let _ = state.bot_manager.stop_bot(&id).await; } let _ = db::delete_bot(&state.db, &id, &q.user_id).await; Ok(StatusCode::NO_CONTENT) } pub async fn start_bot( State(state): State, Path(id): Path, Query(q): Query, ) -> Result, (StatusCode, Json)> { let bot = db::get_bot_owned(&state.db, &id, &q.user_id) .await .map_err(|e| error_json(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()))? .ok_or_else(|| error_json(StatusCode::NOT_FOUND, "bot not found"))?; state .bot_manager .start_bot(&bot.id) .await .map_err(|e| error_json(StatusCode::CONFLICT, &e))?; db::set_bot_status(&state.db, &id, "connecting", None) .await .ok(); let bot = db::get_bot_owned(&state.db, &id, &q.user_id) .await .map_err(|e| error_json(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()))? .ok_or_else(|| error_json(StatusCode::NOT_FOUND, "bot not found"))?; Ok(Json(bot_to_response(&state, bot).await)) } pub async fn stop_bot( State(state): State, Path(id): Path, Query(q): Query, ) -> Result, (StatusCode, Json)> { let _bot = db::get_bot_owned(&state.db, &id, &q.user_id) .await .map_err(|e| error_json(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()))? .ok_or_else(|| error_json(StatusCode::NOT_FOUND, "bot not found"))?; state .bot_manager .stop_bot(&id) .await .map_err(|e| error_json(StatusCode::CONFLICT, &e))?; let bot = db::get_bot_owned(&state.db, &id, &q.user_id) .await .map_err(|e| error_json(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()))? .ok_or_else(|| error_json(StatusCode::NOT_FOUND, "bot not found"))?; Ok(Json(bot_to_response(&state, bot).await)) } // --- Script endpoints --- pub async fn list_scripts( State(state): State, Path(id): Path, Query(q): Query, ) -> Result>, (StatusCode, Json)> { let _bot = db::get_bot_owned(&state.db, &id, &q.user_id) .await .map_err(|e| error_json(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()))? .ok_or_else(|| error_json(StatusCode::NOT_FOUND, "bot not found"))?; let scripts = db::list_scripts(&state.db, &id) .await .map_err(|e| error_json(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()))?; Ok(Json(scripts.into_iter().map(script_to_response).collect())) } pub async fn create_script( State(state): State, Path(id): Path, Json(req): Json, ) -> Result<(StatusCode, Json), (StatusCode, Json)> { let _bot = db::get_bot_owned(&state.db, &id, &req.user_id) .await .map_err(|e| error_json(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()))? .ok_or_else(|| error_json(StatusCode::NOT_FOUND, "bot not found"))?; let sid = nanoid::nanoid!(12); db::insert_script(&state.db, &sid, &id, &req.name, &req.source) .await .map_err(|e| error_json(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()))?; let script = db::get_script(&state.db, &sid) .await .map_err(|e| error_json(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()))? .ok_or_else(|| { error_json( StatusCode::INTERNAL_SERVER_ERROR, "script not found after insert", ) })?; Ok((StatusCode::CREATED, Json(script_to_response(script)))) } pub async fn get_script( State(state): State, Path((id, sid)): Path<(String, String)>, Query(q): Query, ) -> Result, (StatusCode, Json)> { let _bot = db::get_bot_owned(&state.db, &id, &q.user_id) .await .map_err(|e| error_json(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()))? .ok_or_else(|| error_json(StatusCode::NOT_FOUND, "bot not found"))?; let script = db::get_script(&state.db, &sid) .await .map_err(|e| error_json(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()))? .ok_or_else(|| error_json(StatusCode::NOT_FOUND, "script not found"))?; if script.bot_id != id { return Err(error_json(StatusCode::NOT_FOUND, "script not found")); } Ok(Json(script_to_response(script))) } pub async fn update_script( State(state): State, Path((id, sid)): Path<(String, String)>, Json(req): Json, ) -> Result, (StatusCode, Json)> { let _bot = db::get_bot_owned(&state.db, &id, &req.user_id) .await .map_err(|e| error_json(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()))? .ok_or_else(|| error_json(StatusCode::NOT_FOUND, "bot not found"))?; let existing = db::get_script(&state.db, &sid) .await .map_err(|e| error_json(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()))? .ok_or_else(|| error_json(StatusCode::NOT_FOUND, "script not found"))?; if existing.bot_id != id { return Err(error_json(StatusCode::NOT_FOUND, "script not found")); } db::update_script(&state.db, &sid, &req.name, &req.source, req.enabled) .await .map_err(|e| error_json(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()))?; let script = db::get_script(&state.db, &sid) .await .map_err(|e| error_json(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()))? .ok_or_else(|| error_json(StatusCode::NOT_FOUND, "script not found"))?; Ok(Json(script_to_response(script))) } pub async fn delete_script( State(state): State, Path((id, sid)): Path<(String, String)>, Query(q): Query, ) -> Result)> { let _bot = db::get_bot_owned(&state.db, &id, &q.user_id) .await .map_err(|e| error_json(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()))? .ok_or_else(|| error_json(StatusCode::NOT_FOUND, "bot not found"))?; let existing = db::get_script(&state.db, &sid) .await .map_err(|e| error_json(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()))? .ok_or_else(|| error_json(StatusCode::NOT_FOUND, "script not found"))?; if existing.bot_id != id { return Err(error_json(StatusCode::NOT_FOUND, "script not found")); } db::delete_script(&state.db, &sid) .await .map_err(|e| error_json(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()))?; Ok(StatusCode::NO_CONTENT) } // --- Log endpoints --- pub async fn list_logs( State(state): State, Path(id): Path, Query(q): Query, ) -> Result>, (StatusCode, Json)> { let _bot = db::get_bot_owned(&state.db, &id, &q.user_id) .await .map_err(|e| error_json(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()))? .ok_or_else(|| error_json(StatusCode::NOT_FOUND, "bot not found"))?; let logs = db::list_logs(&state.db, &id, 200) .await .map_err(|e| error_json(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()))?; Ok(Json(logs.into_iter().map(log_to_response).collect())) } // --- KV endpoints --- pub async fn list_kv( State(state): State, Path(id): Path, Query(q): Query, ) -> Result>, (StatusCode, Json)> { let _bot = db::get_bot_owned(&state.db, &id, &q.user_id) .await .map_err(|e| error_json(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()))? .ok_or_else(|| error_json(StatusCode::NOT_FOUND, "bot not found"))?; let kvs = db::kv_list(&state.db, &id) .await .map_err(|e| error_json(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()))?; Ok(Json(kvs.into_iter().map(kv_to_response).collect())) } pub async fn set_kv( State(state): State, Path((id, key)): Path<(String, String)>, Json(req): Json, ) -> Result, (StatusCode, Json)> { let _bot = db::get_bot_owned(&state.db, &id, &req.user_id) .await .map_err(|e| error_json(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()))? .ok_or_else(|| error_json(StatusCode::NOT_FOUND, "bot not found"))?; db::kv_set(&state.db, &id, &key, &req.value) .await .map_err(|e| error_json(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()))?; let kv = db::kv_get(&state.db, &id, &key) .await .map_err(|e| error_json(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()))? .ok_or_else(|| error_json(StatusCode::INTERNAL_SERVER_ERROR, "kv not found after set"))?; Ok(Json(kv_to_response(kv))) } pub async fn delete_kv( State(state): State, Path((id, key)): Path<(String, String)>, Query(q): Query, ) -> Result)> { let _bot = db::get_bot_owned(&state.db, &id, &q.user_id) .await .map_err(|e| error_json(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()))? .ok_or_else(|| error_json(StatusCode::NOT_FOUND, "bot not found"))?; let deleted = db::kv_delete(&state.db, &id, &key) .await .map_err(|e| error_json(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()))?; if !deleted { return Err(error_json(StatusCode::NOT_FOUND, "key not found")); } Ok(StatusCode::NO_CONTENT) } // --- Usage endpoint --- pub async fn user_usage( State(state): State, Path(user_id): Path, ) -> Json { let bot_count = db::count_bots(&state.db, &user_id).await.unwrap_or(0); let bots_enabled = sqlx::query_as::<_, (i64,)>( "SELECT COUNT(*) FROM bots WHERE user_id = $1 AND enabled = true", ) .bind(&user_id) .fetch_one(&state.db) .await .map(|r| r.0) .unwrap_or(0); let bots = db::list_bots(&state.db, &user_id).await.unwrap_or_default(); let mut bots_running: i64 = 0; for bot in &bots { if state.bot_manager.is_running(&bot.id).await { bots_running += 1; } } Json(UsageResponse { bot_count, bots_enabled, bots_running, }) }