821537186e
- Reorganized import statements in adapter tests for better readability - Replaced or_insert_with(Vec::new) with or_default() in test closures - Updated Cargo.lock with new dependency versions and checksums - Added TLS features to tonic dependency configuration - Included sqlx, chrono, and uuid dependencies with specific features - Added jsonwebtoken and arc-swap as project dependencies - Reformatted assertion statements to comply with line length limits - Adjusted base64 import order in engine codec module - Updated protobuf include statement formatting
170 lines
5.2 KiB
Rust
170 lines
5.2 KiB
Rust
//! Message read operations — get single, list by channel, list by thread.
|
|
//!
|
|
//! All list queries use UUID v7 cursor-based pagination (no OFFSET).
|
|
|
|
use sqlx::Row;
|
|
use uuid::Uuid;
|
|
|
|
use crate::ImksResult;
|
|
use crate::models::message::Message;
|
|
|
|
use super::message_repo::MessageRepo;
|
|
use super::pagination::{CursorPage, clamp_limit};
|
|
|
|
impl MessageRepo {
|
|
/// Fetch a single message by ID.
|
|
///
|
|
/// Returns `None` if the message doesn't exist or has been soft-deleted.
|
|
pub async fn get(&self, message_id: Uuid) -> ImksResult<Option<Message>> {
|
|
let row = sqlx::query_as::<_, Message>(
|
|
"SELECT * FROM message WHERE id = $1 AND deleted_at IS NULL",
|
|
)
|
|
.bind(message_id)
|
|
.fetch_optional(self.pool())
|
|
.await?;
|
|
|
|
Ok(row)
|
|
}
|
|
|
|
/// List messages in a channel with cursor-based pagination.
|
|
///
|
|
/// Returns messages in reverse chronological order (newest first).
|
|
/// Pass `before` as the last message ID from the previous page to
|
|
/// fetch the next page.
|
|
pub async fn list_by_channel(
|
|
&self,
|
|
channel_id: Uuid,
|
|
before: Option<Uuid>,
|
|
limit: Option<i64>,
|
|
) -> ImksResult<CursorPage<Message>> {
|
|
let effective_limit = clamp_limit(limit);
|
|
// Fetch one extra row to determine `has_more`.
|
|
let fetch_limit = effective_limit + 1;
|
|
|
|
let rows = match before {
|
|
Some(cursor) => {
|
|
sqlx::query_as::<_, Message>(
|
|
r#"
|
|
SELECT * FROM message
|
|
WHERE channel_id = $1
|
|
AND deleted_at IS NULL
|
|
AND id < $2
|
|
ORDER BY id DESC
|
|
LIMIT $3
|
|
"#,
|
|
)
|
|
.bind(channel_id)
|
|
.bind(cursor)
|
|
.bind(fetch_limit)
|
|
.fetch_all(self.pool())
|
|
.await?
|
|
}
|
|
None => {
|
|
sqlx::query_as::<_, Message>(
|
|
r#"
|
|
SELECT * FROM message
|
|
WHERE channel_id = $1
|
|
AND deleted_at IS NULL
|
|
ORDER BY id DESC
|
|
LIMIT $2
|
|
"#,
|
|
)
|
|
.bind(channel_id)
|
|
.bind(fetch_limit)
|
|
.fetch_all(self.pool())
|
|
.await?
|
|
}
|
|
};
|
|
|
|
Ok(CursorPage::from_raw(rows, effective_limit, |m| m.id))
|
|
}
|
|
|
|
/// List messages in a thread with cursor-based pagination.
|
|
pub async fn list_by_thread(
|
|
&self,
|
|
thread_id: Uuid,
|
|
before: Option<Uuid>,
|
|
limit: Option<i64>,
|
|
) -> ImksResult<CursorPage<Message>> {
|
|
let effective_limit = clamp_limit(limit);
|
|
let fetch_limit = effective_limit + 1;
|
|
|
|
let rows = match before {
|
|
Some(cursor) => {
|
|
sqlx::query_as::<_, Message>(
|
|
r#"
|
|
SELECT * FROM message
|
|
WHERE thread_id = $1
|
|
AND deleted_at IS NULL
|
|
AND id < $2
|
|
ORDER BY id DESC
|
|
LIMIT $3
|
|
"#,
|
|
)
|
|
.bind(thread_id)
|
|
.bind(cursor)
|
|
.bind(fetch_limit)
|
|
.fetch_all(self.pool())
|
|
.await?
|
|
}
|
|
None => {
|
|
sqlx::query_as::<_, Message>(
|
|
r#"
|
|
SELECT * FROM message
|
|
WHERE thread_id = $1
|
|
AND deleted_at IS NULL
|
|
ORDER BY id DESC
|
|
LIMIT $2
|
|
"#,
|
|
)
|
|
.bind(thread_id)
|
|
.bind(fetch_limit)
|
|
.fetch_all(self.pool())
|
|
.await?
|
|
}
|
|
};
|
|
|
|
Ok(CursorPage::from_raw(rows, effective_limit, |m| m.id))
|
|
}
|
|
|
|
/// Get message reaction counts grouped by emoji content.
|
|
///
|
|
/// Returns `(content, count)` pairs for the given message.
|
|
pub async fn get_reaction_counts(&self, message_id: Uuid) -> ImksResult<Vec<(String, i64)>> {
|
|
let rows = sqlx::query(
|
|
r#"
|
|
SELECT content, COUNT(*)::BIGINT AS cnt
|
|
FROM message_reaction
|
|
WHERE message_id = $1
|
|
GROUP BY content
|
|
"#,
|
|
)
|
|
.bind(message_id)
|
|
.fetch_all(self.pool())
|
|
.await?;
|
|
|
|
Ok(rows
|
|
.into_iter()
|
|
.map(|r| (r.get("content"), r.get("cnt")))
|
|
.collect())
|
|
}
|
|
|
|
/// Count attachments and embeds for a message.
|
|
pub async fn get_content_counts(&self, message_id: Uuid) -> ImksResult<(i64, i64)> {
|
|
let att_row = sqlx::query(
|
|
"SELECT COUNT(*)::BIGINT AS cnt FROM message_attachment WHERE message_id = $1",
|
|
)
|
|
.bind(message_id)
|
|
.fetch_one(self.pool())
|
|
.await?;
|
|
|
|
let emb_row =
|
|
sqlx::query("SELECT COUNT(*)::BIGINT AS cnt FROM message_embed WHERE message_id = $1")
|
|
.bind(message_id)
|
|
.fetch_one(self.pool())
|
|
.await?;
|
|
|
|
Ok((att_row.get("cnt"), emb_row.get("cnt")))
|
|
}
|
|
}
|