use models::chat::user_drafts; use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter, Set}; use serde::{Deserialize, Serialize}; use time::OffsetDateTime; use utoipa::ToSchema; use uuid::Uuid; use crate::errors::ChatError; use crate::services::service::ChatService; use crate::share::lookup; #[derive(Debug, Clone, Serialize, ToSchema)] pub struct DraftDto { pub room_id: Uuid, pub thread_id: Option, pub content: String, pub updated_at: OffsetDateTime, } #[derive(Debug, Clone, Deserialize, ToSchema)] pub struct UpsertDraftCmd { #[serde(default)] pub thread_id: Option, pub content: String, } impl ChatService { pub async fn upsert_draft( &self, caller: i64, room_id: Uuid, cmd: UpsertDraftCmd, ) -> Result { let _ctx = lookup::require_room_readable(self, room_id, caller).await?; if cmd.content.chars().count() > self.settings.chat.max_message_len { return Err(ChatError::BadRequest("draft too long".to_string())); } let now = OffsetDateTime::now_utc(); let mut existing: Option = None; let candidates = user_drafts::Entity::find() .filter(user_drafts::Column::UserId.eq(caller)) .filter(user_drafts::Column::RoomId.eq(room_id)) .all(self.database.write()) .await?; for row in candidates { if row.thread_id == cmd.thread_id { existing = Some(row); break; } } match existing { Some(row) => { if cmd.content.is_empty() { user_drafts::Entity::delete_by_id(row.id) .exec(self.database.write()) .await?; return Ok(DraftDto { room_id, thread_id: cmd.thread_id, content: String::new(), updated_at: now, }); } let mut am: user_drafts::ActiveModel = row.into(); am.content = Set(cmd.content.clone()); am.updated_at = Set(now); am.update(self.database.write()).await?; } None => { if !cmd.content.is_empty() { user_drafts::ActiveModel { id: Set(Uuid::now_v7()), user_id: Set(caller), room_id: Set(room_id), thread_id: Set(cmd.thread_id), content: Set(cmd.content.clone()), created_at: Set(now), updated_at: Set(now), } .insert(self.database.write()) .await?; } } } Ok(DraftDto { room_id, thread_id: cmd.thread_id, content: cmd.content, updated_at: now, }) } pub async fn get_draft( &self, caller: i64, room_id: Uuid, thread_id: Option, ) -> Result, ChatError> { let _ctx = lookup::require_room_readable(self, room_id, caller).await?; let rows = user_drafts::Entity::find() .filter(user_drafts::Column::UserId.eq(caller)) .filter(user_drafts::Column::RoomId.eq(room_id)) .all(self.database.read()) .await?; Ok(rows .into_iter() .find(|r| r.thread_id == thread_id) .map(|r| DraftDto { room_id, thread_id: r.thread_id, content: r.content, updated_at: r.updated_at, })) } pub async fn delete_draft( &self, caller: i64, room_id: Uuid, thread_id: Option, ) -> Result<(), ChatError> { let _ctx = lookup::require_room_readable(self, room_id, caller).await?; let mut q = user_drafts::Entity::delete_many() .filter(user_drafts::Column::UserId.eq(caller)) .filter(user_drafts::Column::RoomId.eq(room_id)); if let Some(tid) = thread_id { q = q.filter(user_drafts::Column::ThreadId.eq(tid)); } let deleted = q.exec(self.database.write()).await?; if deleted.rows_affected == 0 { return Err(ChatError::NotFound); } Ok(()) } pub async fn list_my_drafts(&self, caller: i64) -> Result, ChatError> { let rows = user_drafts::Entity::find() .filter(user_drafts::Column::UserId.eq(caller)) .all(self.database.read()) .await?; Ok(rows .into_iter() .map(|r| DraftDto { room_id: r.room_id, thread_id: r.thread_id, content: r.content, updated_at: r.updated_at, }) .collect()) } }