use crate::error::RoomError; use crate::service::RoomService; use crate::ws_context::WsUserContext; use chrono::Utc; use models::rooms::{room, room_message, room_thread}; use models::users::user as user_model; use queue::RoomMessageEnvelope; use sea_orm::*; use serde_json; use uuid::Uuid; impl RoomService { pub async fn room_message_list( &self, room_id: Uuid, before_seq: Option, after_seq: Option, limit: Option, ctx: &WsUserContext, ) -> Result { let user_id = ctx.user_id; self.require_room_member(room_id, user_id).await?; let mut query = room_message::Entity::find().filter(room_message::Column::Room.eq(room_id)); if let Some(before_seq) = before_seq { query = query.filter(room_message::Column::Seq.lt(before_seq)); } if let Some(after_seq) = after_seq { query = query.filter(room_message::Column::Seq.gt(after_seq)); } let total = query.clone().count(&self.db).await? as i64; let models = query .order_by_desc(room_message::Column::Seq) .limit(limit.unwrap_or(50)) .all(&self.db) .await?; let user_ids: Vec = models .iter() .filter(|m| m.sender_type.to_string() == "member") .filter_map(|m| m.sender_id) .collect(); let ai_model_ids: Vec = models .iter() .filter(|m| m.sender_type.to_string() == "ai") .filter_map(|m| m.sender_id) .collect(); let users: std::collections::HashMap = if !user_ids.is_empty() { use sea_orm::ColumnTrait; user_model::Entity::find() .filter(user_model::Column::Uid.is_in(user_ids)) .all(&self.db) .await? .into_iter() .map(|u| (u.uid, u.display_name.unwrap_or(u.username))) .collect() } else { std::collections::HashMap::new() }; let ai_names: std::collections::HashMap = if !ai_model_ids.is_empty() { use sea_orm::ColumnTrait; models::agents::model::Entity::find() .filter(models::agents::model::Column::Id.is_in(ai_model_ids)) .all(&self.db) .await? .into_iter() .map(|m| (m.id, m.name)) .collect() } else { std::collections::HashMap::new() }; let mut messages: Vec = models .into_iter() .map(|msg| { let sender_type = msg.sender_type.to_string(); let display_name = match sender_type.as_str() { "ai" => msg.sender_id.and_then(|id| ai_names.get(&id).cloned()), _ => msg.sender_id.and_then(|id| users.get(&id).cloned()), }; super::RoomMessageResponse { id: msg.id, seq: msg.seq, room: msg.room, sender_type, sender_id: msg.sender_id, display_name, thread: msg.thread, in_reply_to: msg.in_reply_to, content: msg.content, content_type: msg.content_type.to_string(), edited_at: msg.edited_at, send_at: msg.send_at, revoked: msg.revoked, revoked_by: msg.revoked_by, } }) .collect(); messages.reverse(); Ok(super::RoomMessageListResponse { messages, total }) } pub async fn room_message_create( &self, room_id: Uuid, request: super::RoomMessageCreateRequest, ctx: &WsUserContext, ) -> Result { let user_id = ctx.user_id; let room_model = self.find_room_or_404(room_id).await?; self.require_room_member(room_id, user_id).await?; let content_type_str = request .content_type .clone() .unwrap_or_else(|| "text".to_string()); Self::parse_message_content_type(Some(content_type_str.clone()))?; Self::validate_content(&request.content, super::MAX_MESSAGE_CONTENT_LEN)?; let content = Self::sanitize_content(&request.content); let thread_id = request.thread; if let Some(tid) = thread_id { let thread = room_thread::Entity::find_by_id(tid) .one(&self.db) .await? .ok_or_else(|| RoomError::NotFound("Thread not found".to_string()))?; if thread.room != room_id { return Err(RoomError::BadRequest("thread not in room".to_string())); } } let seq = self.next_room_message_seq(room_id, &self.db).await?; let now = Utc::now(); let id = Uuid::now_v7(); let project_id = room_model.project; let in_reply_to = request.in_reply_to; let envelope = RoomMessageEnvelope { id, dedup_key: Some(format!("{}:{}", room_id, id)), room_id, sender_type: "member".to_string(), sender_id: Some(user_id), thread_id, in_reply_to, content: content.clone(), content_type: content_type_str.clone(), send_at: now, seq, }; let db = &self.db; let txn = db.begin().await?; self.queue.publish(room_id, envelope).await?; self.room_manager.metrics.messages_sent.increment(1); let mut room_active: room::ActiveModel = room_model.clone().into(); room_active.last_msg_at = Set(now); room_active.update(&txn).await?; if let Some(tid) = thread_id { let thread = room_thread::Entity::find_by_id(tid) .one(&txn) .await? .ok_or_else(|| RoomError::NotFound("Thread not found".to_string()))?; let participants: Vec = serde_json::from_value(thread.participants.clone()).unwrap_or_default(); let participants: Vec = if !participants.contains(&user_id) { let mut p = participants; p.push(user_id); p } else { participants }; let preview = if content.len() > 50 { format!("{}...", &content[..50]) } else { content.clone() }; let mut active: room_thread::ActiveModel = thread.into(); active.last_message_at = Set(now); active.last_message_preview = Set(Some(preview)); active.participants = Set(serde_json::to_value(participants).unwrap_or_default()); active.updated_at = Set(now); active.update(&txn).await?; } txn.commit().await?; self.publish_room_event( project_id, super::RoomEventType::NewMessage, Some(room_id), None, Some(id), Some(seq), ) .await; let mentioned_users = self.resolve_mentions(&request.content).await; for mentioned_user_id in mentioned_users { if mentioned_user_id == user_id { continue; } let _ = self .notification_create(super::NotificationCreateRequest { notification_type: super::NotificationType::Mention, user_id: mentioned_user_id, title: format!("{} 在 {} 中提到了你", user_id, room_model.room_name), content: Some(content.clone()), room_id: Some(room_id), project_id, related_message_id: Some(id), related_user_id: Some(user_id), related_room_id: Some(room_id), metadata: None, expires_at: None, }) .await; } let should_respond = self.should_ai_respond(room_id).await.unwrap_or(false); let is_text_message = request .content_type .as_ref() .map(|ct| ct == "text") .unwrap_or(true); if should_respond && is_text_message { if let Err(e) = self .process_message_ai(room_id, id, user_id, content.clone()) .await { slog::warn!(self.log, "Failed to process AI message: {}", e); } } let display_name = { let user = user_model::Entity::find() .filter(user_model::Column::Uid.eq(user_id)) .one(&self.db) .await .ok() .flatten(); user.map(|u| u.display_name.unwrap_or_else(|| u.username)) }; Ok(super::RoomMessageResponse { id, seq, room: room_id, sender_type: "member".to_string(), sender_id: Some(user_id), display_name, thread: thread_id, in_reply_to, content: request.content, content_type: content_type_str, edited_at: None, send_at: now, revoked: None, revoked_by: None, }) } pub async fn room_message_update( &self, message_id: Uuid, request: super::RoomMessageUpdateRequest, ctx: &WsUserContext, ) -> Result { let user_id = ctx.user_id; Self::validate_content(&request.content, super::MAX_MESSAGE_CONTENT_LEN)?; let model = room_message::Entity::find_by_id(message_id) .one(&self.db) .await? .ok_or_else(|| RoomError::NotFound("Message not found".to_string()))?; self.require_room_member(model.room, user_id).await?; if model.sender_id != Some(user_id) { return Err(RoomError::NoPower); } let elapsed = Utc::now().signed_duration_since(model.send_at); if elapsed.num_minutes() > 120 { return Err(RoomError::BadRequest( "消息只能在发送后 2 小时内编辑".into(), )); } let old_content = model.content.clone(); let new_content = request.content.clone(); self.save_message_edit_history(message_id, user_id, old_content, new_content) .await?; let mut active: room_message::ActiveModel = model.into(); active.content = Set(request.content); active.edited_at = Set(Some(Utc::now())); let updated = active.update(&self.db).await?; let updated_room = updated.room; let room = self.find_room_or_404(updated_room).await?; self.publish_room_event( room.project, super::RoomEventType::MessageEdited, Some(updated_room), None, Some(updated.id), None, ) .await; Ok(self.resolve_display_name(updated, updated_room).await) } pub async fn room_message_revoke( &self, message_id: Uuid, ctx: &WsUserContext, ) -> Result { let user_id = ctx.user_id; let model = room_message::Entity::find_by_id(message_id) .one(&self.db) .await? .ok_or_else(|| RoomError::NotFound("Message not found".to_string()))?; let member = self.require_room_member_model(model.room, user_id).await?; let can_admin = Self::is_room_admin(&member.role); let can_author = model.sender_id == Some(user_id); if !can_admin && !can_author { return Err(RoomError::NoPower); } let mut active: room_message::ActiveModel = model.into(); active.revoked = Set(Some(Utc::now())); active.revoked_by = Set(Some(user_id)); let updated = active.update(&self.db).await?; let updated_room = updated.room; let room = self.find_room_or_404(updated_room).await?; self.publish_room_event( room.project, super::RoomEventType::MessageRevoked, Some(updated_room), None, Some(updated.id), None, ) .await; Ok(self.resolve_display_name(updated, updated_room).await) } pub async fn room_message_get( &self, message_id: Uuid, ctx: &WsUserContext, ) -> Result { let user_id = ctx.user_id; let model = room_message::Entity::find_by_id(message_id) .one(&self.db) .await? .ok_or_else(|| RoomError::NotFound("Message not found".to_string()))?; let room_id = model.room; self.require_room_member(room_id, user_id).await?; Ok(self.resolve_display_name(model, room_id).await) } }