//! Rich embed on a message — new table `message_embed` + `message_embed_field`. //! //! Discord-style embeds: link previews, rich cards with title/description/ //! thumbnail/image/footer/fields. Generated by the server (link preview) //! or sent by bots/webhooks. use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use uuid::Uuid; /// A single embed attached to a message. /// One message can have multiple embeds (e.g. link preview + bot embed). #[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)] pub struct MessageEmbed { pub id: Uuid, pub message_id: Uuid, /// "link" | "article" | "image" | "video" | "rich" pub embed_type: String, pub title: Option, pub description: Option, pub url: Option, /// Embed accent color as integer (Discord format: 0xRRGGBB). pub color: Option, // Media /// Main image URL. pub image_url: Option, pub image_width: Option, pub image_height: Option, /// Small thumbnail URL. pub thumbnail_url: Option, pub thumbnail_width: Option, pub thumbnail_height: Option, /// Video URL (for video embeds). pub video_url: Option, pub video_width: Option, pub video_height: Option, // Footer pub author_name: Option, pub author_url: Option, pub author_icon_url: Option, pub footer_text: Option, pub footer_icon_url: Option, /// Provider name (e.g. "YouTube", "GitHub"). pub provider_name: Option, pub provider_url: Option, pub created_at: DateTime, } /// A key-value field within an embed (Discord-style field rows). /// Stored in a separate table for flexibility. #[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)] pub struct MessageEmbedField { pub id: Uuid, pub embed_id: Uuid, pub name: String, pub value: String, /// Whether this field should display inline (side by side with other inline fields). pub inline: bool, pub position: i32, } /// An embed with its fields resolved in a single structure. /// /// Convenience type returned by read APIs, joining embed and fields. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct EmbedDetail { #[serde(flatten)] pub embed: MessageEmbed, pub fields: Vec, } #[cfg(test)] mod tests { use super::*; #[test] fn test_embed_serialize() { let embed = MessageEmbed { id: Uuid::now_v7(), message_id: Uuid::now_v7(), embed_type: "link".into(), title: Some("Example".into()), description: Some("A link preview".into()), url: Some("https://example.com".into()), color: Some(0x00FF00), image_url: None, image_width: None, image_height: None, thumbnail_url: None, thumbnail_width: None, thumbnail_height: None, video_url: None, video_width: None, video_height: None, author_name: None, author_url: None, author_icon_url: None, footer_text: None, footer_icon_url: None, provider_name: Some("GitHub".into()), provider_url: None, created_at: Utc::now(), }; let json = serde_json::to_value(&embed).unwrap(); assert_eq!(json["embed_type"], "link"); assert_eq!(json["title"], "Example"); } #[test] fn test_embed_detail_serialize() { let embed = MessageEmbed { id: Uuid::now_v7(), message_id: Uuid::now_v7(), embed_type: "rich".into(), title: Some("Article".into()), description: None, url: None, color: None, image_url: None, image_width: None, image_height: None, thumbnail_url: None, thumbnail_width: None, thumbnail_height: None, video_url: None, video_width: None, video_height: None, author_name: None, author_url: None, author_icon_url: None, footer_text: None, footer_icon_url: None, provider_name: None, provider_url: None, created_at: Utc::now(), }; let field = MessageEmbedField { id: Uuid::now_v7(), embed_id: embed.id, name: "Key".into(), value: "Value".into(), inline: true, position: 0, }; let detail = EmbedDetail { embed, fields: vec![field], }; let json = serde_json::to_value(&detail).unwrap(); assert_eq!(json["embed_type"], "rich"); assert!(json["fields"].is_array()); } }