use chrono::Utc; use uuid::Uuid; use crate::event::{RoomInfo, UserInfo, member}; use crate::{ChannelBus, ChannelResult}; use super::WsHandler; use super::WsOutEvent; impl WsHandler { pub(super) async fn subscribe( bus: &ChannelBus, user_id: Uuid, _room: Uuid, ) -> ChannelResult> { bus.refresh_user(user_id).await?; Ok(None) } pub(super) async fn unsubscribe( _bus: &ChannelBus, _user_id: Uuid, _room: Uuid, ) -> ChannelResult> { Ok(None) } pub(super) async fn typing( bus: &ChannelBus, room: Uuid, user_id: Uuid, action: &str, ) -> ChannelResult> { Self::ensure_room_access(bus, user_id, room).await?; let key = (room, user_id); if action == "start" { let ty_room = bus .lookup_room(room) .await .unwrap_or_else(|_| RoomInfo::unknown(room)); let ty_user = bus .lookup_user(user_id) .await .unwrap_or_else(|_| UserInfo::unknown(user_id)); let already_typing = bus.inner.typing_states.contains_key(&key); if let Some((_, (_, _, old_cancel))) = bus.inner.typing_states.remove(&key) { old_cancel.cancel(); } let cancel = tokio_util::sync::CancellationToken::new(); let cancel_clone = cancel.clone(); let bus_clone = bus.clone(); let user_clone = ty_user.clone(); let room_clone = ty_room.clone(); bus.inner .typing_states .insert(key, (ty_user.clone(), ty_room.clone(), cancel)); tokio::spawn(async move { tokio::time::sleep(std::time::Duration::from_secs(10)).await; if cancel_clone.is_cancelled() { return; } bus_clone .inner .typing_states .remove(&(room_clone.id, user_clone.id)); let room_id = room_clone.id; let stop_data = member::TypingStopService { room: room_clone, user: user_clone, sender_type: "user".to_string(), stopped_at: Utc::now(), }; let _ = bus_clone .publish_room_event(room_id, "typing.stop", &stop_data) .await; }); if !already_typing { let data = member::TypingStartService { room: ty_room, user: ty_user, sender_type: "user".to_string(), started_at: Utc::now(), }; bus.publish_room_event(room, "typing.start", &data).await?; return Ok(Some(WsOutEvent::TypingStart { room: data.room.clone(), data, })); } Ok(None) } else { if let Some((_, (_, _, cancel))) = bus.inner.typing_states.remove(&key) { cancel.cancel(); } let ty_room = bus .lookup_room(room) .await .unwrap_or_else(|_| RoomInfo::unknown(room)); let ty_user = bus .lookup_user(user_id) .await .unwrap_or_else(|_| UserInfo::unknown(user_id)); let data = member::TypingStopService { room: ty_room, user: ty_user, sender_type: "user".to_string(), stopped_at: Utc::now(), }; bus.publish_room_event(room, "typing.stop", &data).await?; Ok(Some(WsOutEvent::TypingStop { room: data.room.clone(), data, })) } } pub(super) async fn read_receipt( bus: &ChannelBus, user_id: Uuid, room: Uuid, last_read_seq: i64, ) -> ChannelResult> { Self::ensure_room_access(bus, user_id, room).await?; bus.inner .reconnect .save_client_state(user_id, room, last_read_seq) .await?; db::sqlx::query( "INSERT INTO user_room_state (\"user\", room, last_read_seq, last_read_at, updated_at) \ VALUES ($1, $2, $3, now(), now()) \ ON CONFLICT (\"user\", room) DO UPDATE \ SET last_read_seq = GREATEST(user_room_state.last_read_seq, $3), \ last_read_at = now(), updated_at = now()", ) .bind(user_id) .bind(room) .bind(last_read_seq) .execute(bus.inner.db.writer()) .await?; let rr_room = bus .lookup_room(room) .await .unwrap_or_else(|_| RoomInfo::unknown(room)); let rr_user = bus .lookup_user(user_id) .await .unwrap_or_else(|_| UserInfo::unknown(user_id)); let data = member::ReadReceiptService { room: rr_room.clone(), user: rr_user, last_read_seq, updated_at: Utc::now(), }; bus.publish_room_event(room, "member.read_receipt", &data) .await?; Ok(Some(WsOutEvent::ReadReceipt { room: rr_room, data, })) } pub(super) async fn csrf_token( bus: &ChannelBus, user_id: Uuid, ) -> ChannelResult> { let token = bus.inner.csrf.generate_token(user_id).await?; Ok(Some(WsOutEvent::CsrfToken { token })) } }