1use crate::{
6 auth::middleware::AuthUser,
7 db::turso::Conversation,
8 types::{AppError, Result},
9 AppState,
10};
11use axum::{
12 extract::{Path, State},
13 Json,
14};
15use serde::{Deserialize, Serialize};
16use utoipa::ToSchema;
17
18#[derive(Debug, Serialize, ToSchema)]
20pub struct ConversationSummary {
21 pub id: String,
23 pub title: Option<String>,
25 pub message_count: i32,
27 pub created_at: String,
29 pub updated_at: String,
31}
32
33impl From<Conversation> for ConversationSummary {
34 fn from(c: Conversation) -> Self {
35 Self {
36 id: c.id,
37 title: c.title,
38 message_count: c.message_count,
39 created_at: c.created_at,
40 updated_at: c.updated_at,
41 }
42 }
43}
44
45#[derive(Debug, Serialize, ToSchema)]
47pub struct ConversationDetails {
48 pub id: String,
50 pub title: Option<String>,
52 pub messages: Vec<ConversationMessage>,
54 pub created_at: String,
56 pub updated_at: String,
58}
59
60#[derive(Debug, Serialize, ToSchema)]
62pub struct ConversationMessage {
63 pub id: String,
65 pub role: String,
67 pub content: String,
69 pub created_at: String,
71}
72
73#[derive(Debug, Deserialize, ToSchema)]
75pub struct UpdateConversationRequest {
76 pub title: Option<String>,
78}
79
80#[utoipa::path(
82 get,
83 path = "/api/conversations",
84 responses(
85 (status = 200, description = "List of conversations", body = Vec<ConversationSummary>),
86 (status = 401, description = "Unauthorized")
87 ),
88 tag = "conversations",
89 security(("bearer" = []))
90)]
91pub async fn list_conversations(
92 State(state): State<AppState>,
93 AuthUser(claims): AuthUser,
94) -> Result<Json<Vec<ConversationSummary>>> {
95 let conversations = state.turso.get_user_conversations(&claims.sub).await?;
96
97 let summaries: Vec<ConversationSummary> = conversations
98 .into_iter()
99 .map(ConversationSummary::from)
100 .collect();
101
102 Ok(Json(summaries))
103}
104
105#[utoipa::path(
107 get,
108 path = "/api/conversations/{id}",
109 params(
110 ("id" = String, Path, description = "Conversation ID")
111 ),
112 responses(
113 (status = 200, description = "Conversation details", body = ConversationDetails),
114 (status = 404, description = "Conversation not found"),
115 (status = 401, description = "Unauthorized")
116 ),
117 tag = "conversations",
118 security(("bearer" = []))
119)]
120pub async fn get_conversation(
121 State(state): State<AppState>,
122 AuthUser(claims): AuthUser,
123 Path(id): Path<String>,
124) -> Result<Json<ConversationDetails>> {
125 let conversation = state.turso.get_conversation(&id).await?;
127
128 if conversation.user_id != claims.sub {
129 return Err(AppError::Auth(
130 "Not authorized to access this conversation".to_string(),
131 ));
132 }
133
134 let messages = state.turso.get_conversation_history(&id).await?;
135
136 let message_details: Vec<ConversationMessage> = messages
137 .into_iter()
138 .enumerate()
139 .map(|(idx, msg)| ConversationMessage {
140 id: format!("{}-{}", id, idx), role: format!("{:?}", msg.role).to_lowercase(),
142 content: msg.content,
143 created_at: msg.timestamp.to_rfc3339(),
144 })
145 .collect();
146
147 Ok(Json(ConversationDetails {
148 id: conversation.id,
149 title: conversation.title,
150 messages: message_details,
151 created_at: conversation.created_at,
152 updated_at: conversation.updated_at,
153 }))
154}
155
156#[utoipa::path(
158 put,
159 path = "/api/conversations/{id}",
160 params(
161 ("id" = String, Path, description = "Conversation ID")
162 ),
163 request_body = UpdateConversationRequest,
164 responses(
165 (status = 200, description = "Conversation updated"),
166 (status = 404, description = "Conversation not found"),
167 (status = 401, description = "Unauthorized")
168 ),
169 tag = "conversations",
170 security(("bearer" = []))
171)]
172pub async fn update_conversation(
173 State(state): State<AppState>,
174 AuthUser(claims): AuthUser,
175 Path(id): Path<String>,
176 Json(payload): Json<UpdateConversationRequest>,
177) -> Result<Json<serde_json::Value>> {
178 let conversation = state.turso.get_conversation(&id).await?;
180
181 if conversation.user_id != claims.sub {
182 return Err(AppError::Auth(
183 "Not authorized to modify this conversation".to_string(),
184 ));
185 }
186
187 state
188 .turso
189 .update_conversation_title(&id, payload.title.as_deref())
190 .await?;
191
192 Ok(Json(serde_json::json!({"success": true})))
193}
194
195#[utoipa::path(
197 delete,
198 path = "/api/conversations/{id}",
199 params(
200 ("id" = String, Path, description = "Conversation ID")
201 ),
202 responses(
203 (status = 204, description = "Conversation deleted"),
204 (status = 404, description = "Conversation not found"),
205 (status = 401, description = "Unauthorized")
206 ),
207 tag = "conversations",
208 security(("bearer" = []))
209)]
210pub async fn delete_conversation(
211 State(state): State<AppState>,
212 AuthUser(claims): AuthUser,
213 Path(id): Path<String>,
214) -> Result<axum::http::StatusCode> {
215 let conversation = state.turso.get_conversation(&id).await?;
217
218 if conversation.user_id != claims.sub {
219 return Err(AppError::Auth(
220 "Not authorized to delete this conversation".to_string(),
221 ));
222 }
223
224 state.turso.delete_conversation(&id).await?;
225
226 Ok(axum::http::StatusCode::NO_CONTENT)
227}