saorsa_core/messaging/
media.rs

1// Media processing for rich messaging
2// Handles images, videos, voice messages, and file attachments
3
4use super::types::*;
5use anyhow::Result;
6use blake3::Hasher;
7use serde::{Deserialize, Serialize};
8
9/// Media processor for handling attachments
10pub struct MediaProcessor {
11    /// Maximum file size in bytes (100MB)
12    max_file_size: u64,
13    /// Supported image formats
14    _image_formats: Vec<String>,
15    /// Supported video formats
16    _video_formats: Vec<String>,
17    /// Supported audio formats
18    _audio_formats: Vec<String>,
19}
20
21impl MediaProcessor {
22    /// Create a new media processor
23    pub fn new() -> Result<Self> {
24        Ok(Self {
25            max_file_size: 100 * 1024 * 1024, // 100MB
26            _image_formats: ["jpg", "jpeg", "png", "gif", "webp", "avif", "svg"]
27                .iter()
28                .map(|s| s.to_string())
29                .collect(),
30            _video_formats: ["mp4", "webm", "mov", "avi", "mkv"]
31                .iter()
32                .map(|s| s.to_string())
33                .collect(),
34            _audio_formats: ["mp3", "m4a", "ogg", "wav", "aac", "opus"]
35                .iter()
36                .map(|s| s.to_string())
37                .collect(),
38        })
39    }
40
41    /// Process a raw attachment
42    pub async fn process_attachment(&self, data: Vec<u8>) -> Result<Attachment> {
43        // Check size
44        if data.len() as u64 > self.max_file_size {
45            return Err(anyhow::anyhow!("File too large: {} bytes", data.len()));
46        }
47
48        // Detect MIME type
49        let mime_type = self.detect_mime_type(&data);
50
51        // Generate hash for DHT storage
52        let dht_hash = self.generate_hash(&data);
53
54        // Generate thumbnail if image/video
55        let thumbnail = if self.is_image(&mime_type) || self.is_video(&mime_type) {
56            Some(self.generate_thumbnail(&data, &mime_type).await?)
57        } else {
58            None
59        };
60
61        // Create attachment
62        Ok(Attachment {
63            id: uuid::Uuid::new_v4().to_string(),
64            filename: format!("attachment_{}", chrono::Utc::now().timestamp()),
65            mime_type,
66            size_bytes: data.len() as u64,
67            thumbnail,
68            dht_hash,
69            encryption_key: None, // Will be set by encryption service
70            metadata: std::collections::HashMap::new(),
71        })
72    }
73
74    /// Process an image
75    pub async fn process_image(&self, data: Vec<u8>) -> Result<ProcessedImage> {
76        let mime_type = self.detect_mime_type(&data);
77
78        if !self.is_image(&mime_type) {
79            return Err(anyhow::anyhow!("Not an image file"));
80        }
81
82        // Generate multiple sizes for responsive display
83        let thumbnail = self.resize_image(&data, 150, 150).await?;
84        let preview = self.resize_image(&data, 500, 500).await?;
85        let blurhash = self.generate_blurhash(&data).await?;
86
87        Ok(ProcessedImage {
88            original: data.clone(),
89            thumbnail,
90            preview,
91            blurhash,
92            width: 0, // Would be extracted from image metadata
93            height: 0,
94            mime_type,
95        })
96    }
97
98    /// Process a video
99    pub async fn process_video(&self, data: Vec<u8>) -> Result<ProcessedVideo> {
100        let mime_type = self.detect_mime_type(&data);
101
102        if !self.is_video(&mime_type) {
103            return Err(anyhow::anyhow!("Not a video file"));
104        }
105
106        // Extract video metadata
107        let duration = self.get_video_duration(&data).await?;
108        let thumbnail = self.extract_video_frame(&data, 0.0).await?;
109
110        Ok(ProcessedVideo {
111            data,
112            thumbnail,
113            duration_seconds: duration,
114            width: 0, // Would be extracted from video metadata
115            height: 0,
116            mime_type,
117            streaming_url: None,
118        })
119    }
120
121    /// Process a voice message
122    pub async fn process_voice_message(&self, data: Vec<u8>) -> Result<VoiceMessage> {
123        // Generate waveform for visualization
124        let waveform = self.generate_waveform(&data).await?;
125
126        // Optionally transcribe using speech-to-text
127        let transcription = self.transcribe_audio(&data).await.ok();
128
129        Ok(VoiceMessage {
130            duration_seconds: self.get_audio_duration(&data).await?,
131            waveform,
132            transcription,
133            mime_type: "audio/opus".to_string(),
134            data,
135        })
136    }
137
138    /// Compress media if needed
139    pub async fn compress_if_needed(&self, data: Vec<u8>, mime_type: &str) -> Result<Vec<u8>> {
140        let size = data.len() as u64;
141
142        // Compress large images
143        if self.is_image(mime_type) && size > 5 * 1024 * 1024 {
144            return self.compress_image(data, 85).await;
145        }
146
147        // Compress large videos
148        if self.is_video(mime_type) && size > 20 * 1024 * 1024 {
149            return self.compress_video(data).await;
150        }
151
152        Ok(data)
153    }
154
155    /// Stream large files in chunks
156    pub async fn create_stream(&self, data: Vec<u8>) -> MediaStream {
157        let chunk_size = 1024 * 1024; // 1MB chunks
158        let chunks = data
159            .chunks(chunk_size)
160            .map(|chunk| chunk.to_vec())
161            .collect();
162
163        MediaStream {
164            chunks,
165            total_size: data.len() as u64,
166            chunk_size: chunk_size as u32,
167            mime_type: self.detect_mime_type(&data),
168        }
169    }
170
171    /// Validate media file
172    pub fn validate_media(&self, data: &[u8], expected_type: &MediaType) -> Result<()> {
173        let mime_type = self.detect_mime_type(data);
174
175        match expected_type {
176            MediaType::Image if !self.is_image(&mime_type) => {
177                Err(anyhow::anyhow!("Expected image, got {}", mime_type))
178            }
179            MediaType::Video if !self.is_video(&mime_type) => {
180                Err(anyhow::anyhow!("Expected video, got {}", mime_type))
181            }
182            MediaType::Audio if !self.is_audio(&mime_type) => {
183                Err(anyhow::anyhow!("Expected audio, got {}", mime_type))
184            }
185            _ => Ok(()),
186        }
187    }
188
189    // Helper methods
190
191    fn detect_mime_type(&self, data: &[u8]) -> String {
192        // Simple magic byte detection
193        if data.starts_with(b"\xFF\xD8\xFF") {
194            "image/jpeg".to_string()
195        } else if data.starts_with(b"\x89PNG") {
196            "image/png".to_string()
197        } else if data.starts_with(b"GIF8") {
198            "image/gif".to_string()
199        } else if data.starts_with(b"RIFF") && data[8..12] == *b"WEBP" {
200            "image/webp".to_string()
201        } else if data.len() > 12 && &data[4..12] == b"ftypavif" {
202            "image/avif".to_string()
203        } else if data.len() > 8 && &data[4..8] == b"ftyp" {
204            "video/mp4".to_string()
205        } else {
206            "application/octet-stream".to_string()
207        }
208    }
209
210    fn is_image(&self, mime_type: &str) -> bool {
211        mime_type.starts_with("image/")
212    }
213
214    fn is_video(&self, mime_type: &str) -> bool {
215        mime_type.starts_with("video/")
216    }
217
218    fn is_audio(&self, mime_type: &str) -> bool {
219        mime_type.starts_with("audio/")
220    }
221
222    fn generate_hash(&self, data: &[u8]) -> String {
223        let mut hasher = Hasher::new();
224        hasher.update(data);
225        hasher.finalize().to_hex().to_string()
226    }
227
228    async fn generate_thumbnail(&self, _data: &[u8], _mime_type: &str) -> Result<Vec<u8>> {
229        // In production, use image processing library
230        // For now, return a placeholder
231        Ok(vec![0; 100])
232    }
233
234    async fn resize_image(&self, data: &[u8], _width: u32, _height: u32) -> Result<Vec<u8>> {
235        // In production, use image crate for resizing
236        Ok(data.to_vec())
237    }
238
239    async fn generate_blurhash(&self, _data: &[u8]) -> Result<String> {
240        // In production, use blurhash crate
241        Ok("LEHV6nWB2yk8pyo0adR*.7kCMdnj".to_string())
242    }
243
244    async fn get_video_duration(&self, _data: &[u8]) -> Result<u32> {
245        // In production, use ffmpeg or similar
246        Ok(60) // Mock 60 seconds
247    }
248
249    async fn extract_video_frame(&self, _data: &[u8], _timestamp: f32) -> Result<Vec<u8>> {
250        // In production, use ffmpeg to extract frame
251        Ok(vec![0; 1000])
252    }
253
254    async fn generate_waveform(&self, _data: &[u8]) -> Result<Vec<u8>> {
255        // In production, analyze audio and generate waveform
256        Ok(vec![50; 100]) // Mock waveform data
257    }
258
259    async fn transcribe_audio(&self, _data: &[u8]) -> Result<String> {
260        // In production, use speech-to-text service
261        Ok("Mock transcription".to_string())
262    }
263
264    async fn get_audio_duration(&self, _data: &[u8]) -> Result<u32> {
265        // In production, parse audio metadata
266        Ok(30) // Mock 30 seconds
267    }
268
269    async fn compress_image(&self, data: Vec<u8>, _quality: u8) -> Result<Vec<u8>> {
270        // In production, use image compression
271        Ok(data)
272    }
273
274    async fn compress_video(&self, data: Vec<u8>) -> Result<Vec<u8>> {
275        // In production, use video compression
276        Ok(data)
277    }
278}
279
280/// Processed image with multiple sizes
281#[derive(Debug, Clone)]
282pub struct ProcessedImage {
283    pub original: Vec<u8>,
284    pub thumbnail: Vec<u8>,
285    pub preview: Vec<u8>,
286    pub blurhash: String,
287    pub width: u32,
288    pub height: u32,
289    pub mime_type: String,
290}
291
292/// Processed video
293#[derive(Debug, Clone)]
294pub struct ProcessedVideo {
295    pub data: Vec<u8>,
296    pub thumbnail: Vec<u8>,
297    pub duration_seconds: u32,
298    pub width: u32,
299    pub height: u32,
300    pub mime_type: String,
301    pub streaming_url: Option<String>,
302}
303
304/// Media stream for large files
305#[derive(Debug, Clone)]
306pub struct MediaStream {
307    pub chunks: Vec<Vec<u8>>,
308    pub total_size: u64,
309    pub chunk_size: u32,
310    pub mime_type: String,
311}
312
313/// Media type enum
314#[derive(Debug, Clone, PartialEq)]
315pub enum MediaType {
316    Image,
317    Video,
318    Audio,
319    Document,
320    Other,
321}
322
323/// Media upload progress
324#[derive(Debug, Clone, Serialize, Deserialize)]
325pub struct UploadProgress {
326    pub bytes_uploaded: u64,
327    pub total_bytes: u64,
328    pub percentage: f32,
329    pub estimated_time_remaining: Option<u32>,
330}
331
332#[cfg(test)]
333mod tests {
334    use super::*;
335
336    #[tokio::test]
337    async fn test_media_processor_creation() {
338        let processor = MediaProcessor::new().unwrap();
339        assert_eq!(processor.max_file_size, 100 * 1024 * 1024);
340    }
341
342    #[tokio::test]
343    async fn test_mime_type_detection() {
344        let processor = MediaProcessor::new().unwrap();
345
346        // JPEG magic bytes
347        let jpeg_data = vec![0xFF, 0xD8, 0xFF, 0xE0];
348        assert_eq!(processor.detect_mime_type(&jpeg_data), "image/jpeg");
349
350        // PNG magic bytes
351        let png_data = vec![0x89, 0x50, 0x4E, 0x47];
352        assert_eq!(processor.detect_mime_type(&png_data), "image/png");
353
354        // GIF magic bytes
355        let gif_data = b"GIF89a".to_vec();
356        assert_eq!(processor.detect_mime_type(&gif_data), "image/gif");
357    }
358
359    #[tokio::test]
360    async fn test_file_size_validation() {
361        let processor = MediaProcessor::new().unwrap();
362
363        // File too large
364        let large_data = vec![0; 101 * 1024 * 1024];
365        let result = processor.process_attachment(large_data).await;
366        assert!(result.is_err());
367
368        // File within limit
369        let normal_data = vec![0; 1024];
370        let result = processor.process_attachment(normal_data).await;
371        assert!(result.is_ok());
372    }
373
374    #[tokio::test]
375    async fn test_hash_generation() {
376        let processor = MediaProcessor::new().unwrap();
377
378        let data = b"test data".to_vec();
379        let hash = processor.generate_hash(&data);
380
381        // Blake3 hash should be deterministic
382        assert_eq!(hash.len(), 64); // 32 bytes as hex
383    }
384}