gitdataai/lib/channel/http/handler/conversation.rs

258 lines
8.3 KiB
Rust

use chrono::Utc;
use uuid::Uuid;
use crate::event::{RoomInfo, UserInfo, conversation};
use crate::{ChannelBus, ChannelResult};
use super::WsHandler;
use super::WsOutEvent;
impl WsHandler {
pub(super) async fn conversation_pin(
bus: &ChannelBus,
user_id: Uuid,
room: Uuid,
pin: bool,
) -> ChannelResult<Option<WsOutEvent>> {
Self::ensure_room_access(bus, user_id, room).await?;
let now = Utc::now();
db::sqlx::query(
"INSERT INTO user_room_state (\"user\", room, is_pinned, updated_at) \
VALUES ($1, $2, $3, $4) \
ON CONFLICT (\"user\", room) DO UPDATE \
SET is_pinned = $3, updated_at = $4",
)
.bind(user_id)
.bind(room)
.bind(pin)
.bind(now)
.execute(bus.inner.db.writer())
.await?;
let room_info = bus
.lookup_room(room)
.await
.unwrap_or_else(|_| RoomInfo::unknown(room));
let user_info = bus
.lookup_user(user_id)
.await
.unwrap_or_else(|_| UserInfo::unknown(user_id));
if pin {
let data = conversation::ConversationPinnedService {
user: user_info,
room: room_info.clone(),
pinned_at: now,
};
bus.emit_to_user(user_id, "conversation.pinned", &data)
.await?;
Ok(Some(WsOutEvent::ConversationPinned {
room: room_info,
data,
}))
} else {
let data = conversation::ConversationUnpinnedService {
user: user_info,
room: room_info.clone(),
unpinned_at: now,
};
bus.emit_to_user(user_id, "conversation.unpinned", &data)
.await?;
Ok(Some(WsOutEvent::ConversationUnpinned {
room: room_info,
data,
}))
}
}
pub(super) async fn conversation_mute(
bus: &ChannelBus,
user_id: Uuid,
room: Uuid,
mute: bool,
) -> ChannelResult<Option<WsOutEvent>> {
Self::ensure_room_access(bus, user_id, room).await?;
let now = Utc::now();
db::sqlx::query(
"INSERT INTO user_room_state (\"user\", room, is_muted, updated_at) \
VALUES ($1, $2, $3, $4) \
ON CONFLICT (\"user\", room) DO UPDATE \
SET is_muted = $3, updated_at = $4",
)
.bind(user_id)
.bind(room)
.bind(mute)
.bind(now)
.execute(bus.inner.db.writer())
.await?;
let room_info = bus
.lookup_room(room)
.await
.unwrap_or_else(|_| RoomInfo::unknown(room));
let user_info = bus
.lookup_user(user_id)
.await
.unwrap_or_else(|_| UserInfo::unknown(user_id));
if mute {
let data = conversation::ConversationMutedService {
user: user_info,
room: room_info.clone(),
muted_at: now,
};
bus.emit_to_user(user_id, "conversation.muted", &data)
.await?;
Ok(Some(WsOutEvent::ConversationMuted {
room: room_info,
data,
}))
} else {
let data = conversation::ConversationUnmutedService {
user: user_info,
room: room_info.clone(),
unmuted_at: now,
};
bus.emit_to_user(user_id, "conversation.unmuted", &data)
.await?;
Ok(Some(WsOutEvent::ConversationUnmuted {
room: room_info,
data,
}))
}
}
pub(super) async fn conversation_notify_level(
bus: &ChannelBus,
user_id: Uuid,
room: Uuid,
notify_level: String,
) -> ChannelResult<Option<WsOutEvent>> {
Self::ensure_room_access(bus, user_id, room).await?;
let valid =
matches!(notify_level.as_str(), "all" | "mentions" | "none");
if !valid {
return Err(crate::ChannelError::Internal(
"notify_level must be 'all', 'mentions', or 'none'".to_string(),
));
}
let now = Utc::now();
let old_level: Option<(String,)> = db::sqlx::query_as(
"SELECT notify_level FROM user_room_state \
WHERE \"user\" = $1 AND room = $2",
)
.bind(user_id)
.bind(room)
.fetch_optional(bus.inner.db.reader())
.await?;
let old = old_level.map(|r| r.0).unwrap_or_else(|| "all".to_string());
db::sqlx::query(
"INSERT INTO user_room_state (\"user\", room, notify_level, updated_at) \
VALUES ($1, $2, $3, $4) \
ON CONFLICT (\"user\", room) DO UPDATE \
SET notify_level = $3, updated_at = $4",
)
.bind(user_id)
.bind(room)
.bind(&notify_level)
.bind(now)
.execute(bus.inner.db.writer())
.await?;
let room_info = bus
.lookup_room(room)
.await
.unwrap_or_else(|_| RoomInfo::unknown(room));
let user_info = bus
.lookup_user(user_id)
.await
.unwrap_or_else(|_| UserInfo::unknown(user_id));
let data = conversation::ConversationNotifyLevelChangedService {
user: user_info,
room: room_info.clone(),
old_level: old,
new_level: notify_level,
updated_at: now,
};
bus.emit_to_user(user_id, "conversation.notify_level_changed", &data)
.await?;
Ok(None)
}
pub(super) async fn conversation_list(
bus: &ChannelBus,
user_id: Uuid,
) -> ChannelResult<Option<WsOutEvent>> {
let rooms = crate::rooms::user_rooms(
&bus.inner.db,
&bus.inner.cache,
&bus.inner.config,
user_id,
)
.await?;
if rooms.is_empty() {
return Ok(Some(WsOutEvent::ConversationList { data: vec![] }));
}
let rows = db::sqlx::query_as::<_, (
Uuid, // room id
String, // room name
String, // room type
bool, // is_pinned
bool, // is_muted
String, // notify_level
i64, // last_read_seq
i64, // max seq from room_message
)>(
"SELECT r.id, r.name, r.room_type, \
COALESCE(s.is_pinned, false), \
COALESCE(s.is_muted, false), \
COALESCE(s.notify_level, 'all'), \
COALESCE(s.last_read_seq, 0), \
COALESCE((SELECT MAX(seq) FROM room_message \
WHERE room = r.id AND deleted_at IS NULL), 0) \
FROM room r \
LEFT JOIN user_room_state s ON s.room = r.id AND s.\"user\" = $1 \
WHERE r.id = ANY($2) AND r.deleted_at IS NULL AND r.is_archived = false \
ORDER BY COALESCE(s.is_pinned, false) DESC, r.name",
)
.bind(user_id)
.bind(&rooms)
.fetch_all(bus.inner.db.reader())
.await?;
let summaries: Vec<conversation::ConversationSummary> = rows
.into_iter()
.map(
|(
id,
name,
room_type,
is_pinned,
is_muted,
notify_level,
last_read_seq,
max_seq,
)| {
let unread = (max_seq - last_read_seq).max(0);
conversation::ConversationSummary {
room: id,
room_name: name,
room_type,
is_pinned,
is_muted,
notify_level,
last_read_seq,
max_seq,
unread_count: unread,
last_read_at: None,
}
},
)
.collect();
Ok(Some(WsOutEvent::ConversationList { data: summaries }))
}
}