gitdataai/lib/channel/http/handler/dm.rs
2026-05-30 01:38:40 +08:00

249 lines
8.4 KiB
Rust

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<Option<WsOutEvent>> {
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<Option<WsOutEvent>> {
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<Option<WsOutEvent>> {
let rows = db::sqlx::query_as::<_, (Uuid, Uuid, Uuid, chrono::DateTime<Utc>)>(
"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 }))
}
}