//! File / image attachment on a message — new table `message_attachment`. //! //! Discord-style: each message can have multiple attachments. Files are //! uploaded to S3 first, then the URL is stored here. use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use uuid::Uuid; /// A file or image attachment on a message. /// /// Maps to the `message_attachment` table. Files are uploaded to object /// storage first, then the URL is stored here. #[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)] pub struct MessageAttachment { pub id: Uuid, pub message_id: Uuid, /// Original filename as uploaded by the user. pub filename: String, /// MIME type: "image/png", "application/pdf", etc. pub content_type: Option, /// File size in bytes. pub size: i64, /// Public URL (S3 presigned or CDN). pub url: String, /// S3 / object-store key for backend access. pub storage_key: Option, /// Image / video width in pixels. pub width: Option, /// Image / video height in pixels. pub height: Option, /// Audio / video duration in seconds. pub duration_secs: Option, /// Blurred low-res preview for progressive loading (base64 data URI). pub blurhash: Option, /// Whether this attachment should be rendered as a spoiler (hidden until click). pub spoiler: bool, pub created_at: DateTime, } /// Lightweight attachment summary for list views. /// /// Omits URL and storage_key to reduce payload size. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct AttachmentSummary { pub id: Uuid, pub filename: String, pub content_type: Option, pub size: i64, pub width: Option, pub height: Option, pub spoiler: bool, } impl From for AttachmentSummary { fn from(a: MessageAttachment) -> Self { Self { id: a.id, filename: a.filename, content_type: a.content_type, size: a.size, width: a.width, height: a.height, spoiler: a.spoiler, } } } #[cfg(test)] mod tests { use super::*; #[test] fn test_attachment_conversion() { let att = MessageAttachment { id: Uuid::now_v7(), message_id: Uuid::now_v7(), filename: "photo.png".into(), content_type: Some("image/png".into()), size: 1024, url: "https://cdn.example.com/photo.png".into(), storage_key: None, width: Some(800), height: Some(600), duration_secs: None, blurhash: None, spoiler: false, created_at: Utc::now(), }; let summary: AttachmentSummary = att.into(); assert_eq!(summary.filename, "photo.png"); assert_eq!(summary.size, 1024); assert_eq!(summary.width, Some(800)); } }