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
111 lines
3.5 KiB
Rust
111 lines
3.5 KiB
Rust
//! Pin CRUD operations on `MessageRepo`.
|
|
//!
|
|
//! One row per pinned message. Channels can have multiple pinned messages.
|
|
//! `position` auto-calculated as `MAX(position) + 1` within the channel.
|
|
|
|
use chrono::Utc;
|
|
use sqlx::Row;
|
|
use uuid::Uuid;
|
|
|
|
use crate::ImksResult;
|
|
use crate::models::message_pin::{MessagePin, PinDetail};
|
|
|
|
use super::message_repo::MessageRepo;
|
|
|
|
impl MessageRepo {
|
|
/// Pin a message in a channel. Computes the next position automatically.
|
|
pub async fn pin_message(
|
|
&self,
|
|
channel_id: Uuid,
|
|
message_id: Uuid,
|
|
pinned_by: Uuid,
|
|
) -> ImksResult<MessagePin> {
|
|
let id = Uuid::now_v7();
|
|
let now = Utc::now();
|
|
let mut tx = self.pool().begin().await?;
|
|
|
|
sqlx::query("SELECT pg_advisory_xact_lock(hashtextextended($1, 0))")
|
|
.bind(channel_id.to_string())
|
|
.execute(&mut *tx)
|
|
.await?;
|
|
|
|
let max_pos: Option<i32> = sqlx::query_scalar(
|
|
"SELECT COALESCE(MAX(position), -1) FROM message_pin WHERE channel_id = $1",
|
|
)
|
|
.bind(channel_id)
|
|
.fetch_one(&mut *tx)
|
|
.await?;
|
|
|
|
let position = max_pos.unwrap_or(-1) + 1;
|
|
|
|
let pin = sqlx::query_as::<_, MessagePin>(
|
|
r#"
|
|
INSERT INTO message_pin (id, channel_id, message_id, pinned_by, position, created_at)
|
|
VALUES ($1, $2, $3, $4, $5, $6)
|
|
ON CONFLICT (channel_id, message_id) DO NOTHING
|
|
RETURNING *
|
|
"#,
|
|
)
|
|
.bind(id)
|
|
.bind(channel_id)
|
|
.bind(message_id)
|
|
.bind(pinned_by)
|
|
.bind(position)
|
|
.bind(now)
|
|
.fetch_optional(&mut *tx)
|
|
.await?
|
|
.ok_or_else(|| crate::ImksError::InvalidInput("Message already pinned".into()))?;
|
|
|
|
tx.commit().await?;
|
|
Ok(pin)
|
|
}
|
|
|
|
/// Unpin a message from a channel.
|
|
pub async fn unpin_message(&self, channel_id: Uuid, message_id: Uuid) -> ImksResult<bool> {
|
|
let result =
|
|
sqlx::query("DELETE FROM message_pin WHERE channel_id = $1 AND message_id = $2")
|
|
.bind(channel_id)
|
|
.bind(message_id)
|
|
.execute(self.pool())
|
|
.await?;
|
|
|
|
Ok(result.rows_affected() > 0)
|
|
}
|
|
|
|
/// List all pinned messages in a channel, newest first, joined with message content.
|
|
pub async fn list_pins(&self, channel_id: Uuid) -> ImksResult<Vec<PinDetail>> {
|
|
let rows = sqlx::query(
|
|
r#"
|
|
SELECT p.*, m.body AS message_body, m.author_id AS message_author_id,
|
|
m.created_at AS message_created_at
|
|
FROM message_pin p
|
|
JOIN message m ON m.id = p.message_id
|
|
WHERE p.channel_id = $1 AND m.deleted_at IS NULL
|
|
ORDER BY p.position ASC
|
|
"#,
|
|
)
|
|
.bind(channel_id)
|
|
.fetch_all(self.pool())
|
|
.await?;
|
|
|
|
let result = rows
|
|
.into_iter()
|
|
.map(|row| PinDetail {
|
|
pin: MessagePin {
|
|
id: row.get("id"),
|
|
channel_id: row.get("channel_id"),
|
|
message_id: row.get("message_id"),
|
|
pinned_by: row.get("pinned_by"),
|
|
position: row.get("position"),
|
|
created_at: row.get("created_at"),
|
|
},
|
|
message_body: row.get("message_body"),
|
|
message_author_id: row.get("message_author_id"),
|
|
message_created_at: row.get("message_created_at"),
|
|
})
|
|
.collect();
|
|
|
|
Ok(result)
|
|
}
|
|
}
|