use room::ws_context::WsUserContext; use uuid::Uuid; use crate::error::AppTransportError; use crate::event::{message, presence as pres, search as srch}; use crate::handler::session::TransportSession; use crate::handler::types::WsOutEvent; // ─── Notification ────────────────────────────────────────────────────── pub(crate) async fn notification_mark_read( session: &TransportSession, id: Uuid, ) -> Result, AppTransportError> { let ctx = WsUserContext::new(session.user.user_id); session .service .room .notification_mark_read(id, &ctx) .await .map_err(|e| { tracing::warn!(error = %e, "notification_mark_read failed"); AppTransportError::Internal })?; Ok(None) } pub(crate) async fn notification_mark_all_read( session: &TransportSession, ) -> Result, AppTransportError> { let ctx = WsUserContext::new(session.user.user_id); session .service .room .notification_mark_all_read(&ctx) .await .map_err(|e| { tracing::warn!(error = %e, "notification_mark_all_read failed"); AppTransportError::Internal })?; Ok(None) } pub(crate) async fn notification_archive( session: &TransportSession, id: Uuid, ) -> Result, AppTransportError> { let ctx = WsUserContext::new(session.user.user_id); session .service .room .notification_archive(id, &ctx) .await .map_err(|e| { tracing::warn!(error = %e, "notification_archive failed"); AppTransportError::Internal })?; Ok(None) } // ─── Search ──────────────────────────────────────────────────────────── pub(crate) async fn search( session: &TransportSession, q: String, room: Option, start_time: Option>, end_time: Option>, sender_id: Option, content_type: Option, limit: Option, offset: Option, ) -> Result, AppTransportError> { let ctx = WsUserContext::new(session.user.user_id); let room_id = room.unwrap_or_default(); let req = room::RoomMessageSearchRequest { q: q.clone(), start_time, end_time, sender_id, content_type, limit, offset, }; let res = session .service .room .room_message_search(room_id, req, &ctx) .await .map_err(|e| { tracing::warn!(error = %e, "room_message_search failed"); AppTransportError::Internal })?; let resp = srch::SearchResultService { q, room: room_id, took_ms: 0, messages: res .messages .into_iter() .map(|m| srch::SearchMessageHitService { highlighted_content: m.highlighted_content.unwrap_or_default(), message: message::MessageNewService { id: m.id, seq: m.seq, room: m.room, sender_type: m.sender_type, sender_id: m.sender_id, display_name: m.display_name, thread: m.thread, in_reply_to: m.in_reply_to, content: m.content, content_type: m.content_type, thinking_content: m.thinking_content, thinking_is_chunked: m.thinking_is_chunked, send_at: m.send_at, reactions: None, }, }) .collect(), total: res.total, }; Ok(Some(WsOutEvent::SearchResult { data: resp })) } // ─── Presence ────────────────────────────────────────────────────────── pub(crate) async fn presence_update( session: &TransportSession, status: pres::UserPresenceStatus, ) -> Result, AppTransportError> { let project_id = session.get_current_project().await; let presence_status = match status { pres::UserPresenceStatus::Online => room::presence::PresenceStatus::Online, pres::UserPresenceStatus::Idle => room::presence::PresenceStatus::Idle, pres::UserPresenceStatus::Dnd => room::presence::PresenceStatus::Dnd, pres::UserPresenceStatus::Offline => room::presence::PresenceStatus::Offline, }; let event = session .service .room .set_user_presence(session.user.user_id, project_id, presence_status) .await; if let Some(evt) = event { let transport_status = match evt.status { room::presence::PresenceStatus::Online => pres::UserPresenceStatus::Online, room::presence::PresenceStatus::Idle => pres::UserPresenceStatus::Idle, room::presence::PresenceStatus::Dnd => pres::UserPresenceStatus::Dnd, room::presence::PresenceStatus::Offline => pres::UserPresenceStatus::Offline, }; Ok(Some(WsOutEvent::PresenceChanged { data: pres::PresenceChangedService { user: evt.user_id, project: evt.project_id, status: transport_status, last_seen_at: evt.last_seen_at, }, })) } else { Ok(None) } } pub(crate) async fn custom_status_update( session: &TransportSession, emoji: Option, text: Option, expires_at: Option>, ) -> Result, AppTransportError> { let evt = session.service.room.set_custom_status( session.user.user_id, emoji.clone(), text.clone(), expires_at, ); if let Some(data) = evt { Ok(Some(WsOutEvent::CustomStatusUpdated { data: pres::CustomStatusUpdatedService { user: data.user_id, emoji: data.emoji, text: data.text, expires_at: data.expires_at, }, })) } else { Ok(None) } } // ─── Invite (stubs) ──────────────────────────────────────────────────── pub(crate) async fn invite_create() -> Result, AppTransportError> { tracing::info!("Invite create"); Ok(None) } pub(crate) async fn invite_accept() -> Result, AppTransportError> { tracing::info!("Invite accept"); Ok(None) } pub(crate) async fn invite_revoke() -> Result, AppTransportError> { tracing::info!("Invite revoke"); Ok(None) } // ─── Ban (stubs) ────────────────────────────────────────────────────── pub(crate) async fn ban_create() -> Result, AppTransportError> { tracing::info!("Ban create"); Ok(None) } pub(crate) async fn ban_remove() -> Result, AppTransportError> { tracing::info!("Ban remove"); Ok(None) } // ─── Voice (stubs) ──────────────────────────────────────────────────── pub(crate) async fn voice_join( room: models::RoomId, ) -> Result, AppTransportError> { tracing::info!(%room, "Voice join"); Ok(None) } pub(crate) async fn voice_leave( room: models::RoomId, ) -> Result, AppTransportError> { tracing::info!(%room, "Voice leave"); Ok(None) } pub(crate) async fn voice_mute( room: models::RoomId, muted: bool, ) -> Result, AppTransportError> { tracing::info!(%room, %muted, "Voice mute"); Ok(None) } pub(crate) async fn voice_deaf( room: models::RoomId, deafened: bool, ) -> Result, AppTransportError> { tracing::info!(%room, %deafened, "Voice deaf"); Ok(None) } pub(crate) async fn screen_share( room: models::RoomId, start: bool, ) -> Result, AppTransportError> { tracing::info!(%room, %start, "Screen share"); Ok(None) } // ─── AI ──────────────────────────────────────────────────────────────── pub(crate) async fn ai_list( session: &TransportSession, room: models::RoomId, ) -> Result, AppTransportError> { let ctx = WsUserContext::new(session.user.user_id); let ai_list = session .service .room .room_ai_list(room, &ctx) .await .map_err(|e| { tracing::warn!(error = %e, "room_ai_list failed"); AppTransportError::Internal })?; let data = serde_json::to_value(ai_list).unwrap_or_default(); Ok(Some(WsOutEvent::Response { request_id: Uuid::nil(), data, })) } pub(crate) async fn ai_upsert( session: &TransportSession, room: models::RoomId, model: Uuid, version: Option, system_prompt: Option, temperature: Option, max_tokens: Option, stream: Option, ) -> Result, AppTransportError> { let ctx = WsUserContext::new(session.user.user_id); let req = room::RoomAiUpsertRequest { model, version, system_prompt, temperature, max_tokens, stream, history_limit: None, use_exact: None, think: None, min_score: None, agent_type: None, }; let ai_model = session .service .room .room_ai_upsert(room, req, &ctx) .await .map_err(|e| { tracing::warn!(error = %e, "room_ai_upsert failed"); AppTransportError::Internal })?; let data = serde_json::to_value(ai_model).unwrap_or_default(); Ok(Some(WsOutEvent::Response { request_id: Uuid::nil(), data, })) } pub(crate) async fn ai_delete( session: &TransportSession, room: models::RoomId, agent_id: Uuid, ) -> Result, AppTransportError> { let ctx = WsUserContext::new(session.user.user_id); session .service .room .room_ai_delete(room, agent_id, &ctx) .await .map_err(|e| { tracing::warn!(error = %e, "room_ai_delete failed"); AppTransportError::Internal })?; Ok(None) } pub(crate) async fn ai_stop( session: &TransportSession, room: models::RoomId, ) -> Result, AppTransportError> { let ctx = WsUserContext::new(session.user.user_id); session .service .room .room_ai_stop(room, &ctx) .await .map_err(|e| { tracing::warn!(error = %e, "room_ai_stop failed"); AppTransportError::Internal })?; Ok(None) } // ─── User ────────────────────────────────────────────────────────────── pub(crate) async fn user_summary( session: &TransportSession, username: String, ) -> Result, AppTransportError> { let ctx = session.to_session(); let summary = session .service .user_get_summary(ctx, username) .await .map_err(|e| { tracing::warn!(error = %e, "user_get_summary failed"); AppTransportError::Internal })?; let data = serde_json::to_value(summary).unwrap_or_default(); Ok(Some(WsOutEvent::Response { request_id: Uuid::nil(), data, })) } // ─── State ───────────────────────────────────────────────────────────── pub(crate) async fn state_set_read_seq( session: &TransportSession, room: models::RoomId, last_read_seq: i64, ) -> Result, AppTransportError> { 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(|e| { tracing::warn!(error = %e, "room_user_state_update_read_seq failed"); AppTransportError::Internal })?; Ok(None) } pub(crate) async fn state_update_dnd( session: &TransportSession, room: models::RoomId, do_not_disturb: Option, dnd_start_hour: Option, dnd_end_hour: Option, ) -> Result, AppTransportError> { 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(|e| { tracing::warn!(error = %e, "room_user_state_update_dnd failed"); AppTransportError::Internal })?; Ok(None) }