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
176 lines
5.5 KiB
Rust
176 lines
5.5 KiB
Rust
//! Core message model — maps to PostgreSQL `message` table.
|
|
//!
|
|
//! Discord-style: `body` holds plain text / markdown, rich content lives in
|
|
//! companion tables (attachment, embed, poll, reaction, mention).
|
|
//! IDs are UUID v7 (time-ordered) so `ORDER BY id` = chronological order.
|
|
|
|
use chrono::{DateTime, Utc};
|
|
use serde::{Deserialize, Serialize};
|
|
use uuid::Uuid;
|
|
|
|
/// Discriminator for system / event messages vs. regular user messages.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
|
|
#[serde(rename_all = "snake_case")]
|
|
pub enum MessageType {
|
|
/// Regular user message (text / markdown).
|
|
#[default]
|
|
Text,
|
|
/// System-generated notice (e.g. "user joined the channel").
|
|
System,
|
|
/// Channel event (pinned a message, changed topic, etc.).
|
|
Event,
|
|
/// Forum article / long-form post displayed as waterfall cards.
|
|
Article,
|
|
}
|
|
|
|
impl MessageType {
|
|
pub fn as_str(&self) -> &'static str {
|
|
match self {
|
|
Self::Text => "text",
|
|
Self::System => "system",
|
|
Self::Event => "event",
|
|
Self::Article => "article",
|
|
}
|
|
}
|
|
|
|
pub fn from_str_lossy(s: &str) -> Self {
|
|
match s {
|
|
"system" => Self::System,
|
|
"event" => Self::Event,
|
|
"article" => Self::Article,
|
|
_ => Self::Text,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl std::fmt::Display for MessageType {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
f.write_str(self.as_str())
|
|
}
|
|
}
|
|
|
|
/// Direct mapping of the `message` table row.
|
|
#[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)]
|
|
pub struct Message {
|
|
/// UUID v7 — time-ordered primary key.
|
|
pub id: Uuid,
|
|
pub channel_id: Uuid,
|
|
pub author_id: Uuid,
|
|
/// Thread this message belongs to (NULL = not threaded).
|
|
pub thread_id: Option<Uuid>,
|
|
/// Direct reply reference (NULL = top-level message).
|
|
pub reply_to_message_id: Option<Uuid>,
|
|
/// "text" | "system" | "event"
|
|
pub message_type: String,
|
|
/// Plain text or markdown body.
|
|
pub body: String,
|
|
/// Extensible metadata (flags, locale, interaction ref, etc.).
|
|
pub metadata: Option<serde_json::Value>,
|
|
pub pinned: bool,
|
|
/// True for bot / system generated messages.
|
|
pub system: bool,
|
|
pub edited_at: Option<DateTime<Utc>>,
|
|
pub deleted_at: Option<DateTime<Utc>>,
|
|
pub created_at: DateTime<Utc>,
|
|
pub updated_at: DateTime<Utc>,
|
|
}
|
|
|
|
/// Lightweight author info embedded in [`MessageDetail`].
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct AuthorInfo {
|
|
pub id: Uuid,
|
|
pub username: String,
|
|
pub display_name: Option<String>,
|
|
pub avatar_url: Option<String>,
|
|
pub is_bot: bool,
|
|
}
|
|
|
|
/// Message with resolved author and reaction/attachment aggregates.
|
|
/// Returned by read APIs; never stored directly.
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct MessageDetail {
|
|
#[serde(flatten)]
|
|
pub message: Message,
|
|
pub author: AuthorInfo,
|
|
/// Aggregated reaction counts: `{ "👍": 3, "🎉": 1 }`.
|
|
pub reactions: std::collections::HashMap<String, i64>,
|
|
pub attachment_count: i64,
|
|
pub embed_count: i64,
|
|
/// Whether the current user has bookmarked this message.
|
|
pub bookmarked: bool,
|
|
/// Reply chain depth (0 = top-level).
|
|
pub reply_depth: i32,
|
|
}
|
|
|
|
/// Generate a new UUID v7 (time-ordered) for message IDs.
|
|
pub fn new_message_id() -> Uuid {
|
|
Uuid::now_v7()
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_message_type_roundtrip() {
|
|
let t = MessageType::Text;
|
|
assert_eq!(t.as_str(), "text");
|
|
assert_eq!(MessageType::from_str_lossy("text"), MessageType::Text);
|
|
assert_eq!(MessageType::from_str_lossy("system"), MessageType::System);
|
|
assert_eq!(MessageType::from_str_lossy("unknown"), MessageType::Text);
|
|
}
|
|
|
|
#[test]
|
|
fn test_message_id_ordering() {
|
|
// UUID v7 IDs generated later should sort after earlier ones.
|
|
let a = new_message_id();
|
|
std::thread::sleep(std::time::Duration::from_millis(2));
|
|
let b = new_message_id();
|
|
assert!(b > a, "UUID v7 should be time-ordered");
|
|
}
|
|
|
|
#[test]
|
|
fn test_message_detail_serialize() {
|
|
let msg = Message {
|
|
id: Uuid::now_v7(),
|
|
channel_id: Uuid::now_v7(),
|
|
author_id: Uuid::now_v7(),
|
|
thread_id: None,
|
|
reply_to_message_id: None,
|
|
message_type: "text".to_string(),
|
|
body: "hello world".to_string(),
|
|
metadata: None,
|
|
pinned: false,
|
|
system: false,
|
|
edited_at: None,
|
|
deleted_at: None,
|
|
created_at: Utc::now(),
|
|
updated_at: Utc::now(),
|
|
};
|
|
|
|
let detail = MessageDetail {
|
|
message: msg,
|
|
author: AuthorInfo {
|
|
id: Uuid::now_v7(),
|
|
username: "alice".to_string(),
|
|
display_name: Some("Alice".to_string()),
|
|
avatar_url: None,
|
|
is_bot: false,
|
|
},
|
|
reactions: std::collections::HashMap::from([
|
|
("👍".to_string(), 3),
|
|
("🎉".to_string(), 1),
|
|
]),
|
|
attachment_count: 0,
|
|
embed_count: 0,
|
|
bookmarked: false,
|
|
reply_depth: 0,
|
|
};
|
|
|
|
let json = serde_json::to_value(&detail).unwrap();
|
|
assert_eq!(json["body"], "hello world");
|
|
assert_eq!(json["author"]["username"], "alice");
|
|
assert_eq!(json["reactions"]["👍"], 3);
|
|
}
|
|
}
|