- 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
290 lines
13 KiB
Rust
290 lines
13 KiB
Rust
|
|
use room::ws_context::WsUserContext;
|
|
|
|
use crate::error::AppTransportError;
|
|
use crate::event::{category, message, reaction};
|
|
use super::session::TransportSession;
|
|
use super::types::{WsInMessage, WsOutEvent};
|
|
|
|
pub struct MessageHandler;
|
|
|
|
impl MessageHandler {
|
|
pub async fn handle(
|
|
session: &TransportSession,
|
|
msg: WsInMessage,
|
|
) -> Result<Option<WsOutEvent>, AppTransportError> {
|
|
match msg {
|
|
WsInMessage::Ping => Ok(Some(WsOutEvent::Pong { protocol_version: super::types::WS_PROTOCOL_VERSION })),
|
|
WsInMessage::Subscribe { room } => {
|
|
let sub = session.subscribe_room(room).await?;
|
|
session.subscriptions.insert(room, sub);
|
|
Ok(None)
|
|
}
|
|
WsInMessage::Unsubscribe { room } => {
|
|
session.unsubscribe_room(room).await;
|
|
Ok(None)
|
|
}
|
|
WsInMessage::TypingStart { room } => {
|
|
session.broadcast_typing(room, "start").await;
|
|
Ok(None)
|
|
}
|
|
WsInMessage::TypingStop { room } => {
|
|
session.broadcast_typing(room, "stop").await;
|
|
Ok(None)
|
|
}
|
|
WsInMessage::ReadReceipt { room, last_read_seq } => {
|
|
let ctx = WsUserContext::new(session.user.user_id);
|
|
session.service.room.room_user_state_update_read_seq(
|
|
room,
|
|
last_read_seq,
|
|
&ctx,
|
|
).await.map_err(|_| AppTransportError::Internal)?;
|
|
Ok(None)
|
|
}
|
|
WsInMessage::MessageCreate { room, content, content_type, thread, in_reply_to } => {
|
|
let ctx = WsUserContext::new(session.user.user_id);
|
|
let msg = session.service.room.room_message_create(
|
|
room,
|
|
room::RoomMessageCreateRequest {
|
|
content,
|
|
content_type,
|
|
thread,
|
|
in_reply_to,
|
|
attachment_ids: vec![],
|
|
},
|
|
&ctx,
|
|
).await.map_err(|_| AppTransportError::Internal)?;
|
|
Ok(Some(WsOutEvent::MessageNew {
|
|
room_id: room,
|
|
data: message::MessageNewService {
|
|
id: msg.id,
|
|
seq: msg.seq,
|
|
room: msg.room,
|
|
sender_type: msg.sender_type,
|
|
sender_id: msg.sender_id,
|
|
display_name: msg.display_name,
|
|
thread: msg.thread,
|
|
in_reply_to: msg.in_reply_to,
|
|
content: msg.content,
|
|
content_type: msg.content_type,
|
|
thinking_content: msg.thinking_content,
|
|
thinking_is_chunked: false,
|
|
send_at: msg.send_at,
|
|
},
|
|
}))
|
|
}
|
|
WsInMessage::MessageUpdate { message, content } => {
|
|
let ctx = WsUserContext::new(session.user.user_id);
|
|
session.service.room.room_message_update(
|
|
message,
|
|
room::RoomMessageUpdateRequest { content },
|
|
&ctx,
|
|
).await.map_err(|_| AppTransportError::Internal)?;
|
|
Ok(None)
|
|
}
|
|
WsInMessage::MessageRevoke { message } => {
|
|
let ctx = WsUserContext::new(session.user.user_id);
|
|
session.service.room.room_message_revoke(message, &ctx)
|
|
.await.map_err(|_| AppTransportError::Internal)?;
|
|
Ok(None)
|
|
}
|
|
WsInMessage::RoomCreate { project, room_name, public, category } => {
|
|
let ctx = WsUserContext::new(session.user.user_id);
|
|
let rm = session.service.room.room_create(
|
|
project.to_string(),
|
|
room::RoomCreateRequest { room_name, public, category },
|
|
&ctx,
|
|
).await.map_err(|_| AppTransportError::Internal)?;
|
|
Ok(Some(WsOutEvent::RoomCreated {
|
|
room_id: rm.id,
|
|
data: crate::event::rooms::RoomCreatedService {
|
|
id: rm.id,
|
|
project,
|
|
room_name: rm.room_name,
|
|
public: rm.public,
|
|
category,
|
|
created_by: session.user.user_id,
|
|
created_at: rm.created_at,
|
|
},
|
|
}))
|
|
}
|
|
WsInMessage::RoomUpdate { room, room_name, public, category } => {
|
|
let ctx = WsUserContext::new(session.user.user_id);
|
|
session.service.room.room_update(
|
|
room,
|
|
room::RoomUpdateRequest { room_name, public, category },
|
|
&ctx,
|
|
).await.map_err(|_| AppTransportError::Internal)?;
|
|
Ok(None)
|
|
}
|
|
WsInMessage::RoomDelete { room } => {
|
|
let ctx = WsUserContext::new(session.user.user_id);
|
|
session.service.room.room_delete(room, &ctx)
|
|
.await.map_err(|_| AppTransportError::Internal)?;
|
|
Ok(None)
|
|
}
|
|
WsInMessage::CategoryCreate { project, name, position } => {
|
|
let ctx = WsUserContext::new(session.user.user_id);
|
|
let cat = session.service.room.room_category_create(
|
|
project.to_string(),
|
|
room::RoomCategoryCreateRequest { name, position },
|
|
&ctx,
|
|
).await.map_err(|_| AppTransportError::Internal)?;
|
|
Ok(Some(WsOutEvent::CategoryCreated {
|
|
project,
|
|
data: category::CategoryCreatedService {
|
|
id: cat.id,
|
|
project,
|
|
name: cat.name,
|
|
position: cat.position,
|
|
created_by: session.user.user_id,
|
|
created_at: cat.created_at,
|
|
},
|
|
}))
|
|
}
|
|
WsInMessage::CategoryUpdate { id, name, position } => {
|
|
let ctx = WsUserContext::new(session.user.user_id);
|
|
session.service.room.room_category_update(
|
|
id,
|
|
room::RoomCategoryUpdateRequest { name, position },
|
|
&ctx,
|
|
).await.map_err(|_| AppTransportError::Internal)?;
|
|
Ok(None)
|
|
}
|
|
WsInMessage::CategoryDelete { id } => {
|
|
let ctx = WsUserContext::new(session.user.user_id);
|
|
session.service.room.room_category_delete(id, &ctx)
|
|
.await.map_err(|_| AppTransportError::Internal)?;
|
|
Ok(None)
|
|
}
|
|
WsInMessage::AccessGrant { room, user } => {
|
|
let ctx = WsUserContext::new(session.user.user_id);
|
|
session.service.room.room_access_grant(room, user, &ctx)
|
|
.await.map_err(|_| AppTransportError::Internal)?;
|
|
Ok(None)
|
|
}
|
|
WsInMessage::AccessRevoke { room, user } => {
|
|
let ctx = WsUserContext::new(session.user.user_id);
|
|
session.service.room.room_access_revoke(room, user, &ctx)
|
|
.await.map_err(|_| AppTransportError::Internal)?;
|
|
Ok(None)
|
|
}
|
|
WsInMessage::ReactionAdd { room, message, emoji } => {
|
|
let ctx = WsUserContext::new(session.user.user_id);
|
|
let rxs = session.service.room.message_reaction_add(message, emoji, &ctx)
|
|
.await.map_err(|_| AppTransportError::Internal)?;
|
|
Ok(Some(WsOutEvent::ReactionBatchUpdated {
|
|
room_id: room,
|
|
data: reaction::ReactionBatchUpdatedService {
|
|
room: room,
|
|
message: message,
|
|
reactions: rxs.reactions.into_iter().map(|g| reaction::ReactionGroup {
|
|
emoji: g.emoji, count: g.count as i64, reacted_by_me: g.reacted_by_me,
|
|
users: g.users.iter().filter_map(|u| u.parse::<uuid::Uuid>().ok()).collect(),
|
|
}).collect(),
|
|
},
|
|
}))
|
|
}
|
|
WsInMessage::ReactionRemove { room, message, emoji } => {
|
|
let ctx = WsUserContext::new(session.user.user_id);
|
|
let rxs = session.service.room.message_reaction_remove(message, emoji, &ctx)
|
|
.await.map_err(|_| AppTransportError::Internal)?;
|
|
Ok(Some(WsOutEvent::ReactionBatchUpdated {
|
|
room_id: room,
|
|
data: reaction::ReactionBatchUpdatedService {
|
|
room: room,
|
|
message: message,
|
|
reactions: rxs.reactions.into_iter().map(|g| reaction::ReactionGroup {
|
|
emoji: g.emoji, count: g.count as i64, reacted_by_me: g.reacted_by_me,
|
|
users: g.users.iter().filter_map(|u| u.parse::<uuid::Uuid>().ok()).collect(),
|
|
}).collect(),
|
|
},
|
|
}))
|
|
}
|
|
WsInMessage::ThreadCreate { room, parent } => {
|
|
let ctx = WsUserContext::new(session.user.user_id);
|
|
session.service.room.room_thread_create(
|
|
room,
|
|
room::RoomThreadCreateRequest { parent_seq: parent },
|
|
&ctx,
|
|
).await.map_err(|_| AppTransportError::Internal)?;
|
|
Ok(None)
|
|
}
|
|
WsInMessage::ThreadResolve { .. } => Ok(None),
|
|
WsInMessage::ThreadArchive { .. } => Ok(None),
|
|
WsInMessage::PinAdd { room: _, message } => {
|
|
let ctx = WsUserContext::new(session.user.user_id);
|
|
session.service.room.room_pin_add(message, &ctx)
|
|
.await.map_err(|_| AppTransportError::Internal)?;
|
|
Ok(None)
|
|
}
|
|
WsInMessage::PinRemove { room: _, message } => {
|
|
let ctx = WsUserContext::new(session.user.user_id);
|
|
session.service.room.room_pin_remove(message, &ctx)
|
|
.await.map_err(|_| AppTransportError::Internal)?;
|
|
Ok(None)
|
|
}
|
|
WsInMessage::DraftSave { .. } | WsInMessage::DraftClear { .. } => {
|
|
// TODO: draft service not yet implemented in room crate
|
|
Ok(None)
|
|
}
|
|
WsInMessage::NotificationMarkRead { id } => {
|
|
let ctx = WsUserContext::new(session.user.user_id);
|
|
session.service.room.notification_mark_read(id, &ctx)
|
|
.await.map_err(|_| AppTransportError::Internal)?;
|
|
Ok(None)
|
|
}
|
|
WsInMessage::NotificationMarkAllRead { .. } => {
|
|
let ctx = WsUserContext::new(session.user.user_id);
|
|
session.service.room.notification_mark_all_read(&ctx)
|
|
.await.map_err(|_| AppTransportError::Internal)?;
|
|
Ok(None)
|
|
}
|
|
WsInMessage::NotificationArchive { id } => {
|
|
let ctx = WsUserContext::new(session.user.user_id);
|
|
session.service.room.notification_archive(id, &ctx)
|
|
.await.map_err(|_| AppTransportError::Internal)?;
|
|
Ok(None)
|
|
}
|
|
// ── Placeholder actions (TODO: implement service calls) ──
|
|
WsInMessage::Search { .. } => Ok(None),
|
|
WsInMessage::PresenceUpdate { .. } => Ok(None),
|
|
WsInMessage::CustomStatusUpdate { .. } => Ok(None),
|
|
WsInMessage::InviteCreate { .. } => Ok(None),
|
|
WsInMessage::InviteAccept { .. } => Ok(None),
|
|
WsInMessage::InviteRevoke { .. } => Ok(None),
|
|
WsInMessage::BanCreate { .. } => Ok(None),
|
|
WsInMessage::BanRemove { .. } => Ok(None),
|
|
WsInMessage::VoiceJoin { .. } => Ok(None),
|
|
WsInMessage::VoiceLeave { .. } => Ok(None),
|
|
WsInMessage::VoiceMute { .. } => Ok(None),
|
|
WsInMessage::VoiceDeaf { .. } => Ok(None),
|
|
WsInMessage::ScreenShare { .. } => Ok(None),
|
|
WsInMessage::AiList { .. } => Ok(None),
|
|
WsInMessage::AiUpsert { .. } => Ok(None),
|
|
WsInMessage::AiDelete { .. } => Ok(None),
|
|
WsInMessage::StateSetReadSeq { room, last_read_seq } => {
|
|
let ctx = WsUserContext::new(session.user.user_id);
|
|
session.service.room.room_user_state_update_read_seq(
|
|
room,
|
|
last_read_seq,
|
|
&ctx,
|
|
).await.map_err(|_| AppTransportError::Internal)?;
|
|
Ok(None)
|
|
}
|
|
WsInMessage::StateUpdateDnd { room, do_not_disturb, dnd_start_hour, dnd_end_hour } => {
|
|
let ctx = WsUserContext::new(session.user.user_id);
|
|
session.service.room.room_user_state_update_dnd(
|
|
room,
|
|
room::RoomUserStateUpdateDndRequest {
|
|
do_not_disturb,
|
|
dnd_start_hour,
|
|
dnd_end_hour,
|
|
},
|
|
&ctx,
|
|
).await.map_err(|_| AppTransportError::Internal)?;
|
|
Ok(None)
|
|
}
|
|
}
|
|
}
|
|
} |