Files
zhenyi 821537186e refactor(tests): reformat code and update dependency management
- 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
2026-06-11 12:11:05 +08:00

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);
}
}