- Add gitignore and prettier configuration files for project scaffolding - Implement room access control service with project member verification - Create user access key management with CRUD operations and activity logging - Add accordion UI component for frontend expandable sections - Implement room AI configuration with list, upsert, and delete operations - Add AI event types for agent join/leave/status change tracking - Create streaming AI processing services for mode and react patterns - Build room AI service with model detection and idempotency handling - Integrate chat service orchestration for AI message processing - Add typing indicators and stream cancellation for AI interactions - Implement mention parsing and context extraction for AI agents
146 lines
5.6 KiB
Rust
146 lines
5.6 KiB
Rust
use models::projects::{project, project_history_name, project_members};
|
|
use models::rooms::room;
|
|
use sea_orm::*;
|
|
use uuid::Uuid;
|
|
|
|
use super::RoomService;
|
|
use crate::error::RoomError;
|
|
|
|
impl RoomService {
|
|
pub(crate) fn validate_name(name: &str, max_len: usize) -> Result<(), RoomError> {
|
|
if name.trim().is_empty() {
|
|
return Err(RoomError::BadRequest("name cannot be empty".to_string()));
|
|
}
|
|
if name.len() > max_len {
|
|
return Err(RoomError::BadRequest(format!(
|
|
"name exceeds maximum length of {} characters",
|
|
max_len
|
|
)));
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
pub(crate) fn validate_content(content: &str, max_len: usize) -> Result<(), RoomError> {
|
|
if content.trim().is_empty() {
|
|
return Err(RoomError::BadRequest("content cannot be empty".to_string()));
|
|
}
|
|
if content.len() > max_len {
|
|
return Err(RoomError::BadRequest(format!(
|
|
"content exceeds maximum length of {} characters",
|
|
max_len
|
|
)));
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
pub(crate) fn sanitize_content(content: &str) -> String {
|
|
ammonia::clean(content)
|
|
}
|
|
|
|
pub(crate) fn parse_message_content_type(
|
|
content_type: Option<String>,
|
|
) -> Result<models::rooms::MessageContentType, RoomError> {
|
|
match content_type.unwrap_or_else(|| "text".to_string()).to_lowercase().as_str() {
|
|
"text" => Ok(models::rooms::MessageContentType::Text),
|
|
"image" => Ok(models::rooms::MessageContentType::Image),
|
|
"audio" => Ok(models::rooms::MessageContentType::Audio),
|
|
"video" => Ok(models::rooms::MessageContentType::Video),
|
|
"file" => Ok(models::rooms::MessageContentType::File),
|
|
_ => Err(RoomError::BadRequest("invalid message content_type".to_string())),
|
|
}
|
|
}
|
|
|
|
pub async fn utils_find_project_by_name(&self, name: String) -> Result<project::Model, RoomError> {
|
|
match project::Entity::find()
|
|
.filter(project::Column::Name.eq(name.clone()))
|
|
.one(&self.db)
|
|
.await
|
|
.inspect_err(|e| tracing::warn!(error = %e, project_name = %name, "utils_find_project_by_name: DB error"))
|
|
.ok()
|
|
.flatten()
|
|
{
|
|
Some(project) => Ok(project),
|
|
None => match project_history_name::Entity::find()
|
|
.filter(project_history_name::Column::HistoryName.eq(name.clone()))
|
|
.one(&self.db)
|
|
.await
|
|
.inspect_err(|e| tracing::warn!(error = %e, name = %name, "project_history_name lookup failed"))
|
|
.ok()
|
|
.flatten()
|
|
{
|
|
Some(project) => self.utils_find_project_by_uid(project.project_uid).await,
|
|
None => Err(RoomError::NotFound("Project not found".to_string())),
|
|
},
|
|
}
|
|
}
|
|
|
|
pub async fn utils_find_project_by_uid(&self, uid: Uuid) -> Result<project::Model, RoomError> {
|
|
project::Entity::find_by_id(uid)
|
|
.one(&self.db)
|
|
.await
|
|
.inspect_err(|e| tracing::warn!(error = %e, project_uid = %uid, "utils_find_project_by_uid: DB error"))
|
|
.ok()
|
|
.flatten()
|
|
.ok_or_else(|| RoomError::NotFound("Project not found".to_string()))
|
|
}
|
|
|
|
pub async fn check_project_access(&self, project_uid: Uuid, user_uid: Uuid) -> Result<(), RoomError> {
|
|
let project = project::Entity::find_by_id(project_uid)
|
|
.one(&self.db)
|
|
.await
|
|
.inspect_err(|e| tracing::warn!(error = %e, project_uid = %project_uid, "check_project_access: DB error"))
|
|
.ok()
|
|
.flatten()
|
|
.ok_or_else(|| RoomError::NotFound("Project not found".to_string()))?;
|
|
|
|
if project.is_public {
|
|
return Ok(());
|
|
}
|
|
|
|
let member = project_members::Entity::find()
|
|
.filter(project_members::Column::Project.eq(project_uid))
|
|
.filter(project_members::Column::User.eq(user_uid))
|
|
.one(&self.db)
|
|
.await?;
|
|
|
|
if member.is_some() { Ok(()) } else { Err(RoomError::NoPower) }
|
|
}
|
|
|
|
pub async fn ensure_room_visible_for_user(
|
|
&self, room: &room::Model, user_id: Uuid,
|
|
) -> Result<(), RoomError> {
|
|
self.require_room_access(room.id, user_id).await
|
|
}
|
|
|
|
pub async fn get_room_version(&self, room_id: Uuid) -> Result<i64, RoomError> {
|
|
let version_key = format!("room:version:{}", room_id);
|
|
let mut conn = self.cache.conn().await.map_err(|e| {
|
|
RoomError::Internal(format!("failed to get redis for version: {}", e))
|
|
})?;
|
|
let version: Option<i64> = redis::cmd("GET")
|
|
.arg(&version_key)
|
|
.query_async(&mut conn)
|
|
.await
|
|
.map_err(|e| RoomError::Internal(format!("version GET: {}", e)))?;
|
|
Ok(version.unwrap_or(0))
|
|
}
|
|
|
|
pub async fn increment_room_version(&self, room_id: Uuid) -> Result<i64, RoomError> {
|
|
Self::raw_increment_room_version(&self.cache, room_id).await
|
|
}
|
|
|
|
pub async fn raw_increment_room_version(
|
|
cache: &db::cache::AppCache, room_id: Uuid,
|
|
) -> Result<i64, RoomError> {
|
|
let version_key = format!("room:version:{}", room_id);
|
|
let mut conn = cache.conn().await.map_err(|e| {
|
|
RoomError::Internal(format!("failed to get redis for version: {}", e))
|
|
})?;
|
|
let version: i64 = redis::cmd("INCR")
|
|
.arg(&version_key)
|
|
.query_async(&mut conn)
|
|
.await
|
|
.map_err(|e| RoomError::Internal(format!("version INCR: {}", e)))?;
|
|
Ok(version)
|
|
}
|
|
} |