use chrono::{DateTime, Utc};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use crate::identity::ConversationId;
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
pub struct MessageId(pub String);
impl MessageId {
pub fn new(id: impl Into<String>) -> Self {
Self(id.into())
}
}
impl std::fmt::Display for MessageId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
pub struct ThreadId(pub String);
impl ThreadId {
pub fn new(id: impl Into<String>) -> Self {
Self(id.into())
}
}
impl std::fmt::Display for ThreadId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct ChannelMessage {
pub id: MessageId,
pub conversation: ConversationId,
pub author: String,
pub content: MessageContent,
pub thread_id: Option<ThreadId>,
pub reply_to: Option<MessageId>,
pub timestamp: DateTime<Utc>,
pub attachments: Vec<Attachment>,
pub metadata: HashMap<String, String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub enum MessageContent {
Text(String),
RichText {
markdown: String,
fallback_plain: String,
},
Media(MediaPayload),
Embed(EmbedPayload),
Mixed(Vec<MessageContent>),
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct Attachment {
pub filename: String,
pub content_type: String,
pub url: String,
pub size_bytes: Option<u64>,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct MediaPayload {
pub media_type: MediaType,
pub url: String,
pub caption: Option<String>,
pub thumbnail_url: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub enum MediaType {
Image,
Video,
Audio,
Document,
Sticker,
GIF,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct EmbedPayload {
pub title: Option<String>,
pub description: Option<String>,
pub url: Option<String>,
pub color: Option<u32>,
pub fields: Vec<EmbedField>,
pub thumbnail: Option<String>,
pub footer: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct EmbedField {
pub name: String,
pub value: String,
pub inline: bool,
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashMap;
fn sample_message() -> ChannelMessage {
ChannelMessage {
id: MessageId::new("msg-001"),
conversation: ConversationId {
platform: "discord".to_string(),
channel_id: "general".to_string(),
server_id: Some("srv-1".to_string()),
},
author: "alice".to_string(),
content: MessageContent::Text("Hello, world!".to_string()),
thread_id: None,
reply_to: None,
timestamp: Utc::now(),
attachments: vec![],
metadata: HashMap::new(),
}
}
#[test]
fn channel_message_serde_roundtrip() {
let msg = sample_message();
let json = serde_json::to_string(&msg).unwrap();
let deserialized: ChannelMessage = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized.id, msg.id);
assert_eq!(deserialized.author, msg.author);
}
#[test]
fn rich_text_serde_roundtrip() {
let content = MessageContent::RichText {
markdown: "**bold**".to_string(),
fallback_plain: "bold".to_string(),
};
let json = serde_json::to_string(&content).unwrap();
let deserialized: MessageContent = serde_json::from_str(&json).unwrap();
match deserialized {
MessageContent::RichText {
markdown,
fallback_plain,
} => {
assert_eq!(markdown, "**bold**");
assert_eq!(fallback_plain, "bold");
}
_ => panic!("expected RichText variant"),
}
}
#[test]
fn mixed_content_serde_roundtrip() {
let content = MessageContent::Mixed(vec![
MessageContent::Text("check this out".to_string()),
MessageContent::Media(MediaPayload {
media_type: MediaType::Image,
url: "https://example.com/image.png".to_string(),
caption: Some("A cool image".to_string()),
thumbnail_url: None,
}),
]);
let json = serde_json::to_string(&content).unwrap();
let deserialized: MessageContent = serde_json::from_str(&json).unwrap();
match deserialized {
MessageContent::Mixed(items) => assert_eq!(items.len(), 2),
_ => panic!("expected Mixed variant"),
}
}
#[test]
fn embed_serde_roundtrip() {
let embed = EmbedPayload {
title: Some("Title".to_string()),
description: Some("Description".to_string()),
url: Some("https://example.com".to_string()),
color: Some(0xFF5733),
fields: vec![EmbedField {
name: "Field".to_string(),
value: "Value".to_string(),
inline: true,
}],
thumbnail: None,
footer: Some("Footer".to_string()),
};
let json = serde_json::to_string(&embed).unwrap();
let deserialized: EmbedPayload = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized.title, embed.title);
assert_eq!(deserialized.fields.len(), 1);
}
#[test]
fn attachment_serde_roundtrip() {
let att = Attachment {
filename: "report.pdf".to_string(),
content_type: "application/pdf".to_string(),
url: "https://example.com/report.pdf".to_string(),
size_bytes: Some(1024),
};
let json = serde_json::to_string(&att).unwrap();
let deserialized: Attachment = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized.filename, "report.pdf");
}
}