mod access; mod access_write; mod ai_common; mod ai_mode_streaming; mod ai_mode_streaming_post; mod ai_mode_streaming_steps; mod ai_nonstreaming; mod ai_react_nonstreaming; mod ai_react_streaming; mod ai_react_streaming_post; mod ai_react_streaming_steps; mod ai_service; mod ai_streaming; mod history; mod mentions; mod notifications; mod patterns; mod process_ai; mod sequence; mod type_convert; mod validation; mod workers; mod workers_spawn; pub use patterns::{mention_bracket_re, mention_tag_re, user_mention_re}; pub use access::{ check_project_member, check_room_access, find_room_or_404, get_or_create_room_user_state, is_project_admin, is_room_admin, require_project_admin, require_room_access, require_room_admin, }; pub use ai_common::create_and_publish_ai_message; pub use ai_nonstreaming::process_message_ai_nonstreaming; pub use ai_react_nonstreaming::process_message_ai_react_nonstreaming; pub use ai_react_streaming::process_message_ai_react_streaming; pub use ai_service::RoomAiService; pub use ai_streaming::process_message_ai_streaming; pub use history::{extract_mention_context, get_room_ai_config, get_room_history, get_user_names}; pub use mentions::extract_mentions; pub use notifications::{notify_project_members, publish_room_event}; pub use sequence::next_room_message_seq_internal; pub use workers::{PushNotificationFn, start_workers}; pub use workers_spawn::{spawn_agent_task, spawn_room_workers, unmark_room_spawned}; use std::sync::Arc; use chrono::Utc; use db::cache::AppCache; use db::database::AppDatabase; use models::rooms::room; use models::rooms::room_ai; use queue::{MessageProducer, ProjectRoomEvent}; use sea_orm::{ColumnTrait, QueryFilter}; use uuid::Uuid; use crate::connection::{DedupCache, RoomConnectionManager}; use crate::error::RoomError; use crate::presence::PresenceStore; use agent::TaskService; use agent::chat::ChatService; use agent::embed::EmbedService; use models::agent_task::AgentType; const DEFAULT_MAX_CONCURRENT_WORKERS: usize = 1024; #[derive(Clone)] pub struct RoomService { pub db: AppDatabase, pub cache: AppCache, pub config: config::AppConfig, pub room_manager: Arc, pub queue: MessageProducer, pub nats: Option>, pub chat_service: Option>, pub task_service: Option>, pub embed_service: Option>, pub push_fn: Option, pub ai_service: RoomAiService, pub presence: PresenceStore, worker_semaphore: Arc, dedup_cache: DedupCache, } impl RoomService { pub fn new( db: AppDatabase, cache: AppCache, config: config::AppConfig, queue: MessageProducer, room_manager: Arc, nats: Option>, chat_service: Option>, task_service: Option>, max_concurrent_workers: Option, push_fn: Option, embed_service: Option>, ) -> Self { let dedup_cache: DedupCache = Arc::new(dashmap::DashMap::with_capacity_and_hasher( 10000, Default::default(), )); let ai_service = RoomAiService::new( db.clone(), cache.clone(), config.clone(), queue.clone(), room_manager.clone(), chat_service.clone(), ); Self { db, cache, config, room_manager, queue, nats, chat_service, task_service, embed_service, ai_service, worker_semaphore: Arc::new(tokio::sync::Semaphore::new( max_concurrent_workers.unwrap_or(DEFAULT_MAX_CONCURRENT_WORKERS), )), dedup_cache, push_fn, presence: PresenceStore::new(), } } pub async fn start_workers( &self, shutdown_rx: tokio::sync::broadcast::Receiver<()>, ) -> anyhow::Result<()> { workers::start_workers( self.db.clone(), self.cache.clone(), self.room_manager.clone(), self.queue.clone(), self.nats.clone(), self.dedup_cache.clone(), self.task_service.clone(), None, // max_concurrent_workers handled by semaphore shutdown_rx, self.embed_service.clone(), ) .await } pub async fn spawn_agent_task( &self, project_id: Uuid, agent_type: AgentType, input: String, _title: Option, execute: F, ) -> anyhow::Result where F: FnOnce(i64, Arc) -> Fut + Send + 'static, Fut: std::future::Future> + Send, { let task_service = match &self.task_service { Some(ts) => ts.clone(), None => return Err(anyhow::anyhow!("task service not configured")), }; workers_spawn::spawn_agent_task( project_id, agent_type, input, task_service, self.queue.clone(), self.room_manager.clone(), self.worker_semaphore.clone(), execute, ) .await } pub fn spawn_room_workers(&self, room_id: uuid::Uuid) { workers_spawn::spawn_room_workers( room_id, self.db.clone(), self.room_manager.clone(), self.queue.clone(), self.nats.clone(), self.worker_semaphore.clone(), self.embed_service.clone(), ); } pub async fn publish_room_event( &self, project_id: uuid::Uuid, event_type: super::RoomEventType, room_id: Option, category_id: Option, message_id: Option, seq: Option, ) { let event = ProjectRoomEvent { event_type: event_type.as_str().into(), project_id, room_id, category_id, message_id, seq, timestamp: Utc::now(), }; self.queue .publish_project_room_event(project_id, event) .await; } pub fn notify_project_members( &self, project_id: uuid::Uuid, notification_type: super::NotificationType, title: String, content: Option, related_room_id: Option, ) { notifications::notify_project_members( self.db.clone(), project_id, notification_type, title, content, related_room_id, ); } pub fn extract_mentions(content: &str) -> Vec { mentions::extract_mentions(content) } pub async fn resolve_mentions(&self, content: &str) -> Vec { use models::users::User; use sea_orm::EntityTrait; let mut resolved = Vec::new(); let mut seen_usernames = Vec::new(); for cap in mention_bracket_re().captures_iter(content) { if let (Some(type_m), Some(id_m)) = (cap.get(1), cap.get(2)) { if type_m.as_str() == "user" { let id = id_m.as_str().trim(); if let Ok(uuid) = Uuid::parse_str(id) { if !resolved.contains(&uuid) { resolved.push(uuid); } } else if let Some(label_m) = cap.get(3) { let label = label_m.as_str().trim(); if !label.is_empty() { let label_lower = label.to_lowercase(); if seen_usernames.contains(&label_lower) { continue; } seen_usernames.push(label_lower.clone()); if let Some(user) = User::find() .filter(models::users::user::Column::Username.ilike(&label_lower)) .one(&self.db) .await .ok() .flatten() { if !resolved.contains(&user.uid) { resolved.push(user.uid); } } } } } } } resolved } pub async fn check_room_access(&self, room_id: Uuid, user_id: Uuid) -> Result<(), RoomError> { access::check_room_access(&self.db, room_id, user_id).await } pub async fn check_project_member( &self, project_id: Uuid, user_id: Uuid, ) -> Result<(), RoomError> { access::check_project_member(&self.db, project_id, user_id).await } pub async fn should_ai_respond(&self, room_id: Uuid, content: &str) -> Result { self.ai_service.should_respond(room_id, content).await } pub async fn get_room_ai_config( &self, room_id: Uuid, ) -> Result, RoomError> { history::get_room_ai_config(&self.db, room_id).await } pub async fn get_user_names( &self, user_ids: &[Uuid], ) -> std::collections::HashMap { history::get_user_names(&self.db, user_ids).await } pub async fn require_room_access(&self, room_id: Uuid, user_id: Uuid) -> Result<(), RoomError> { access::require_room_access(&self.db, room_id, user_id).await } pub async fn require_room_admin(&self, room_id: Uuid, user_id: Uuid) -> Result<(), RoomError> { access::require_room_admin(&self.db, room_id, user_id).await } pub async fn is_room_admin(&self, room_id: Uuid, user_id: Uuid) -> bool { access::is_room_admin(&self.db, room_id, user_id).await } pub async fn require_project_admin( &self, project_id: Uuid, user_id: Uuid, ) -> Result<(), RoomError> { access::require_project_admin(&self.db, project_id, user_id).await } pub async fn find_room_or_404(&self, room_id: Uuid) -> Result { access::find_room_or_404(&self.db, room_id).await } // ─── Presence Methods ───────────────────────────────────────────────────── /// Set user presence in a project context and broadcast to project subscribers. /// Returns the local PresenceChanged type - caller is responsible for conversion. pub async fn set_user_presence( &self, user_id: Uuid, project_id: Option, status: crate::presence::PresenceStatus, ) -> Option { let event = self.presence.set_presence(user_id, project_id, status); if event.is_some() { // Broadcast to project subscribers if let Some(pid) = project_id { self.room_manager .broadcast_project( pid, queue::ProjectRoomEvent { event_type: "presence_changed".into(), project_id: pid, room_id: None, category_id: None, message_id: None, seq: None, timestamp: chrono::Utc::now(), }, ) .await; } } event } /// Set user custom status (emoji, text). pub fn set_custom_status( &self, user_id: Uuid, emoji: Option, text: Option, expires_at: Option>, ) -> Option { self.presence .set_custom_status(user_id, emoji, text, expires_at) } /// Get all presence entries for a project. pub fn get_project_presence(&self, project_id: Uuid) -> Vec { self.presence.get_project_presence(project_id) } /// Remove user presence when they disconnect. pub async fn remove_user_presence( &self, user_id: Uuid, project_id: Option, ) -> Option { let event = self.presence.remove_presence(user_id, project_id); if event.is_some() { // Broadcast to project subscribers if let Some(pid) = project_id { self.room_manager .broadcast_project( pid, queue::ProjectRoomEvent { event_type: "presence_changed".into(), project_id: pid, room_id: None, category_id: None, message_id: None, seq: None, timestamp: chrono::Utc::now(), }, ) .await; } } event } }