use axum::extract::{Path, Query, State}; use axum::Json; use cm_db::repo::threads::{Thread, ThreadMessage}; use cm_domain::AgentId; use serde::Deserialize; use serde_json::{json, Value}; use uuid::Uuid; use crate::routes::claws::workspace_agent; use crate::{ApiError, AppState, Authed}; #[derive(Deserialize)] pub struct ThreadsQuery { #[serde(rename = "clawId")] claw_id: AgentId, } /// GET /api/claw-chat/threads?clawId= — the inter-agent inbox (§7.2). pub async fn threads( State(state): State, Authed(user): Authed, Query(query): Query, ) -> Result>, ApiError> { let agent = workspace_agent(&state, &user, query.claw_id).await?; Ok(Json( cm_db::repo::threads::list_for_agent(&state.pool, agent.id).await?, )) } #[derive(Deserialize)] pub struct MessagesQuery { #[serde(rename = "clawId")] claw_id: AgentId, #[serde(rename = "threadId")] thread_id: Uuid, } /// GET /api/claw-chat/messages?clawId=&threadId= — thread detail (§7.2). pub async fn messages( State(state): State, Authed(user): Authed, Query(query): Query, ) -> Result>, ApiError> { let agent = workspace_agent(&state, &user, query.claw_id).await?; if !cm_db::repo::threads::is_participant(&state.pool, query.thread_id, agent.id).await? { return Err(ApiError::NotFound); } Ok(Json( cm_db::repo::threads::messages(&state.pool, query.thread_id).await?, )) } /// Confirms a thread is in the caller's workspace (room API scoping). async fn workspace_thread( state: &AppState, user: &cm_auth::AuthedUser, thread_id: Uuid, ) -> Result<(), ApiError> { match cm_db::repo::threads::workspace_of(&state.pool, thread_id).await? { Some(ws) if ws == user.workspace_id.as_uuid() => Ok(()), _ => Err(ApiError::NotFound), } } #[derive(Deserialize)] pub struct CreateRoomBody { subject: String, members: Vec, } /// POST /api/claw-chat/rooms — create an N-way group room (operator action). pub async fn create_room( State(state): State, Authed(user): Authed, Json(body): Json, ) -> Result, ApiError> { // Every member must be a claw in the caller's workspace. for id in &body.members { workspace_agent(&state, &user, *id).await?; } let thread_id = cm_db::repo::threads::create_room( &state.pool, user.workspace_id, &body.subject, None, &body.members, ) .await?; Ok(Json(json!({ "threadId": thread_id }))) } /// GET /api/claw-chat/rooms — list the workspace's group rooms. pub async fn rooms( State(state): State, Authed(user): Authed, ) -> Result>, ApiError> { Ok(Json( cm_db::repo::threads::list_rooms_for_workspace(&state.pool, user.workspace_id).await?, )) } #[derive(Deserialize)] pub struct AddParticipantBody { #[serde(rename = "clawId")] claw_id: AgentId, } /// POST /api/claw-chat/rooms/{threadId}/participants — add a claw to a room. pub async fn add_participant( State(state): State, Authed(user): Authed, Path(thread_id): Path, Json(body): Json, ) -> Result, ApiError> { workspace_thread(&state, &user, thread_id).await?; workspace_agent(&state, &user, body.claw_id).await?; cm_db::repo::threads::add_participant(&state.pool, thread_id, body.claw_id, None).await?; Ok(Json(json!({ "ok": true }))) } /// DELETE /api/claw-chat/rooms/{threadId}/participants/{clawId} — remove a claw. pub async fn remove_participant( State(state): State, Authed(user): Authed, Path((thread_id, claw_id)): Path<(Uuid, AgentId)>, ) -> Result, ApiError> { workspace_thread(&state, &user, thread_id).await?; workspace_agent(&state, &user, claw_id).await?; cm_db::repo::threads::remove_participant(&state.pool, thread_id, claw_id).await?; Ok(Json(json!({ "ok": true }))) }