use chrono::Utc; use uuid::Uuid; use crate::event::{RoomInfo, UserInfo, dm}; use crate::{ChannelBus, ChannelResult}; use super::WsOutEvent; use super::WsHandler; impl WsHandler { pub(super) async fn dm_create( bus: &ChannelBus, user_id: Uuid, recipient: Uuid, ) -> ChannelResult> { if user_id == recipient { return Err(crate::ChannelError::Internal( "cannot create DM with yourself".to_string(), )); } let recipient_exists: Option<(Uuid,)> = db::sqlx::query_as( "SELECT id FROM \"user\" WHERE id = $1", ) .bind(recipient) .fetch_optional(bus.inner.db.reader()) .await?; if recipient_exists.is_none() { return Err(crate::ChannelError::UserNotFound); } let (initiator, other) = if user_id < recipient { (user_id, recipient) } else { (recipient, user_id) }; let existing: Option<(Uuid, Uuid, bool)> = db::sqlx::query_as( "SELECT room, initiator, is_closed FROM dm_conversation \ WHERE initiator = $1 AND recipient = $2", ) .bind(initiator) .bind(other) .fetch_optional(bus.inner.db.reader()) .await?; let now = Utc::now(); let (room_id, is_reopen) = if let Some((room, _, is_closed)) = existing { if is_closed { db::sqlx::query( "UPDATE dm_conversation SET is_closed = false, closed_at = NULL, \ updated_at = now() WHERE initiator = $1 AND recipient = $2", ) .bind(initiator) .bind(other) .execute(bus.inner.db.writer()) .await?; db::sqlx::query( "UPDATE room SET is_archived = false, updated_at = now() WHERE id = $1", ) .bind(room) .execute(bus.inner.db.writer()) .await?; (room, true) } else { (room, false) } } else { let shared_wk: Option<(Uuid,)> = db::sqlx::query_as( "SELECT wm1.wk FROM wk_member wm1 \ INNER JOIN wk_member wm2 ON wm2.wk = wm1.wk \ WHERE wm1.\"user\" = $1 AND wm1.leave_at IS NULL \ AND wm2.\"user\" = $2 AND wm2.leave_at IS NULL \ LIMIT 1", ) .bind(user_id) .bind(recipient) .fetch_optional(bus.inner.db.reader()) .await?; let wk = shared_wk.map(|r| r.0).unwrap_or_else(|| { Uuid::nil() }); let room_id = Uuid::new_v4(); db::sqlx::query( "INSERT INTO room (id, wk, name, topic, room_type, position, is_private, \ created_by, created_at, updated_at) \ VALUES ($1, $2, $3, NULL, 'DM', 0, true, $4, now(), now())", ) .bind(room_id) .bind(wk) .bind(format!("dm-{}", &room_id.to_string()[..8])) .bind(user_id) .execute(bus.inner.db.writer()) .await?; db::sqlx::query( "INSERT INTO dm_conversation (room, initiator, recipient, created_at, updated_at) \ VALUES ($1, $2, $3, now(), now()) \ ON CONFLICT (initiator, recipient) DO NOTHING", ) .bind(room_id) .bind(initiator) .bind(other) .execute(bus.inner.db.writer()) .await?; for uid in &[user_id, recipient] { db::sqlx::query( "INSERT INTO room_permission_overwrite \ (room, target_type, target_id, allow_mask, deny_mask, created_at) \ VALUES ($1, 'user', $2, 0, 0, now()) \ ON CONFLICT DO NOTHING", ) .bind(room_id) .bind(uid) .execute(bus.inner.db.writer()) .await?; } (room_id, false) }; let _ = crate::rooms::refresh_user_rooms_cache( &bus.inner.db, &bus.inner.cache, &bus.inner.config, user_id, ) .await; let _ = crate::rooms::refresh_user_rooms_cache( &bus.inner.db, &bus.inner.cache, &bus.inner.config, recipient, ) .await; let room_info = bus.lookup_room(room_id).await.unwrap_or_else(|_| RoomInfo::unknown(room_id)); let initiator_info = bus.lookup_user(user_id).await.unwrap_or_else(|_| UserInfo::unknown(user_id)); let recipient_info = bus.lookup_user(recipient).await.unwrap_or_else(|_| UserInfo::unknown(recipient)); if is_reopen { let data = dm::DmReopenedService { room: room_info.clone(), reopened_by: initiator_info, reopened_at: now, }; bus.emit_to_user(user_id, "dm.reopened", &data).await?; bus.emit_to_user(recipient, "dm.reopened", &data).await?; Ok(Some(WsOutEvent::DmReopened { room: room_info, data, })) } else { let data = dm::DmCreatedService { room: room_info.clone(), initiator: initiator_info, recipient: recipient_info, created_at: now, }; bus.emit_to_user(user_id, "dm.created", &data).await?; bus.emit_to_user(recipient, "dm.created", &data).await?; Ok(Some(WsOutEvent::DmCreated { room: room_info, data, })) } } pub(super) async fn dm_close( bus: &ChannelBus, user_id: Uuid, room: Uuid, ) -> ChannelResult> { let now = Utc::now(); let result = db::sqlx::query( "UPDATE dm_conversation SET is_closed = true, closed_at = $1, updated_at = $1 \ WHERE room = $2 AND (initiator = $3 OR recipient = $3) AND is_closed = false", ) .bind(now) .bind(room) .bind(user_id) .execute(bus.inner.db.writer()) .await?; if result.rows_affected() == 0 { return Ok(None); } let room_info = bus.lookup_room(room).await.unwrap_or_else(|_| RoomInfo::unknown(room)); let closed_by = bus.lookup_user(user_id).await.unwrap_or_else(|_| UserInfo::unknown(user_id)); let data = dm::DmClosedService { room: room_info.clone(), closed_by, closed_at: now, }; bus.publish_room_event(room, "dm.closed", &data).await?; Ok(Some(WsOutEvent::DmClosed { room: room_info, data, })) } pub(super) async fn dm_list( bus: &ChannelBus, user_id: Uuid, ) -> ChannelResult> { let rows = db::sqlx::query_as::<_, (Uuid, Uuid, Uuid, chrono::DateTime)>( "SELECT dc.room, dc.initiator, dc.recipient, dc.created_at \ FROM dm_conversation dc \ INNER JOIN room r ON r.id = dc.room \ WHERE (dc.initiator = $1 OR dc.recipient = $1) \ AND dc.is_closed = false \ AND r.deleted_at IS NULL \ ORDER BY dc.updated_at DESC", ) .bind(user_id) .fetch_all(bus.inner.db.reader()) .await?; let mut results = Vec::with_capacity(rows.len()); for (room_id, initiator_id, recipient_id, created_at) in rows { let room_info = bus .lookup_room(room_id) .await .unwrap_or_else(|_| RoomInfo::unknown(room_id)); let initiator_info = bus .lookup_user(initiator_id) .await .unwrap_or_else(|_| UserInfo::unknown(initiator_id)); let recipient_info = bus .lookup_user(recipient_id) .await .unwrap_or_else(|_| UserInfo::unknown(recipient_id)); results.push(dm::DmCreatedService { room: room_info, initiator: initiator_info, recipient: recipient_info, created_at, }); } Ok(Some(WsOutEvent::DmList { data: results })) } }