deltachat/download/
post_msg_metadata.rs

1use anyhow::{Context as _, Result};
2use num_traits::ToPrimitive;
3use serde::{Deserialize, Serialize};
4
5use crate::context::Context;
6use crate::log::warn;
7use crate::message::Message;
8use crate::message::Viewtype;
9use crate::param::{Param, Params};
10
11/// Metadata contained in Pre-Message that describes the Post-Message.
12#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
13pub struct PostMsgMetadata {
14    /// size of the attachment in bytes
15    pub(crate) size: u64,
16    /// Real viewtype of message
17    pub(crate) viewtype: Viewtype,
18    /// the original file name
19    pub(crate) filename: String,
20    /// Width and height of the image or video
21    #[serde(skip_serializing_if = "Option::is_none")]
22    pub(crate) wh: Option<(i32, i32)>,
23    /// Duration of audio file or video in milliseconds
24    #[serde(skip_serializing_if = "Option::is_none")]
25    pub(crate) duration: Option<i32>,
26}
27
28impl PostMsgMetadata {
29    /// Returns `PostMsgMetadata` for messages with file attachment and `None` otherwise.
30    pub(crate) async fn from_msg(context: &Context, message: &Message) -> Result<Option<Self>> {
31        if !message.viewtype.has_file() {
32            return Ok(None);
33        }
34
35        let size = message
36            .get_filebytes(context)
37            .await?
38            .context("Unexpected: file has no size")?;
39        let filename = message
40            .param
41            .get(Param::Filename)
42            .unwrap_or_default()
43            .to_owned();
44        let wh = {
45            match (
46                message.param.get_int(Param::Width),
47                message.param.get_int(Param::Height),
48            ) {
49                (None, None) => None,
50                (Some(width), Some(height)) => Some((width, height)),
51                wh => {
52                    warn!(
53                        context,
54                        "Message {} misses width or height: {:?}.", message.id, wh
55                    );
56                    None
57                }
58            }
59        };
60        let duration = message.param.get_int(Param::Duration);
61
62        Ok(Some(Self {
63            size,
64            filename,
65            viewtype: message.viewtype,
66            wh,
67            duration,
68        }))
69    }
70
71    pub(crate) fn to_header_value(&self) -> Result<String> {
72        Ok(serde_json::to_string(&self)?)
73    }
74
75    pub(crate) fn try_from_header_value(value: &str) -> Result<Self> {
76        Ok(serde_json::from_str(value)?)
77    }
78}
79
80impl Params {
81    /// Applies data from post_msg_metadata to Params
82    pub(crate) fn apply_post_msg_metadata(
83        &mut self,
84        post_msg_metadata: &PostMsgMetadata,
85    ) -> &mut Self {
86        self.set(Param::PostMessageFileBytes, post_msg_metadata.size);
87        if !post_msg_metadata.filename.is_empty() {
88            self.set(Param::Filename, &post_msg_metadata.filename);
89        }
90        self.set_i64(
91            Param::PostMessageViewtype,
92            post_msg_metadata.viewtype.to_i64().unwrap_or_default(),
93        );
94        if let Some((width, height)) = post_msg_metadata.wh {
95            self.set(Param::Width, width);
96            self.set(Param::Height, height);
97        }
98        if let Some(duration) = post_msg_metadata.duration {
99            self.set(Param::Duration, duration);
100        }
101
102        self
103    }
104}
105
106#[cfg(test)]
107mod tests {
108    use anyhow::Result;
109    use pretty_assertions::assert_eq;
110
111    use crate::{
112        message::{Message, Viewtype},
113        test_utils::{TestContextManager, create_test_image},
114    };
115
116    use super::PostMsgMetadata;
117
118    /// Build from message with file attachment
119    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
120    async fn test_build_from_file_msg() -> Result<()> {
121        let mut tcm = TestContextManager::new();
122        let alice = &tcm.alice().await;
123
124        let mut file_msg = Message::new(Viewtype::File);
125        file_msg.set_file_from_bytes(alice, "test.bin", &vec![0u8; 1_000_000], None)?;
126        let post_msg_metadata = PostMsgMetadata::from_msg(alice, &file_msg).await?;
127        assert_eq!(
128            post_msg_metadata,
129            Some(PostMsgMetadata {
130                size: 1_000_000,
131                viewtype: Viewtype::File,
132                filename: "test.bin".to_string(),
133                wh: None,
134                duration: None,
135            })
136        );
137        Ok(())
138    }
139
140    /// Build from message with image attachment
141    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
142    async fn test_build_from_image_msg() -> Result<()> {
143        let mut tcm = TestContextManager::new();
144        let alice = &tcm.alice().await;
145        let mut image_msg = Message::new(Viewtype::Image);
146
147        let (width, height) = (1080, 1920);
148        let test_img = create_test_image(width, height)?;
149        image_msg.set_file_from_bytes(alice, "vacation.png", &test_img, None)?;
150        // this is usually done while sending,
151        // but we don't send it here, so we need to call it ourself
152        image_msg.try_calc_and_set_dimensions(alice).await?;
153        let post_msg_metadata = PostMsgMetadata::from_msg(alice, &image_msg).await?;
154        assert_eq!(
155            post_msg_metadata,
156            Some(PostMsgMetadata {
157                size: 1816098,
158                viewtype: Viewtype::Image,
159                filename: "vacation.png".to_string(),
160                wh: Some((width as i32, height as i32)),
161                duration: None,
162            })
163        );
164
165        Ok(())
166    }
167
168    /// Test that serialisation results in expected format
169    #[test]
170    fn test_serialize_to_header() -> Result<()> {
171        assert_eq!(
172            PostMsgMetadata {
173                size: 1_000_000,
174                viewtype: Viewtype::File,
175                filename: "test.bin".to_string(),
176                wh: None,
177                duration: None,
178            }
179            .to_header_value()?,
180            "{\"size\":1000000,\"viewtype\":\"File\",\"filename\":\"test.bin\"}"
181        );
182        assert_eq!(
183            PostMsgMetadata {
184                size: 5_342_765,
185                viewtype: Viewtype::Image,
186                filename: "vacation.png".to_string(),
187                wh: Some((1080, 1920)),
188                duration: None,
189            }
190            .to_header_value()?,
191            "{\"size\":5342765,\"viewtype\":\"Image\",\"filename\":\"vacation.png\",\"wh\":[1080,1920]}"
192        );
193        assert_eq!(
194            PostMsgMetadata {
195                size: 5_000,
196                viewtype: Viewtype::Audio,
197                filename: "audio-DD-MM-YY.ogg".to_string(),
198                wh: None,
199                duration: Some(152_310),
200            }
201            .to_header_value()?,
202            "{\"size\":5000,\"viewtype\":\"Audio\",\"filename\":\"audio-DD-MM-YY.ogg\",\"duration\":152310}"
203        );
204
205        Ok(())
206    }
207
208    /// Test that deserialisation from expected format works
209    /// This test will become important for compatibility between versions in the future
210    #[test]
211    fn test_deserialize_from_header() -> Result<()> {
212        assert_eq!(
213            serde_json::from_str::<PostMsgMetadata>(
214                "{\"size\":1000000,\"viewtype\":\"File\",\"filename\":\"test.bin\",\"wh\":null,\"duration\":null}"
215            )?,
216            PostMsgMetadata {
217                size: 1_000_000,
218                viewtype: Viewtype::File,
219                filename: "test.bin".to_string(),
220                wh: None,
221                duration: None,
222            }
223        );
224        assert_eq!(
225            serde_json::from_str::<PostMsgMetadata>(
226                "{\"size\":5342765,\"viewtype\":\"Image\",\"filename\":\"vacation.png\",\"wh\":[1080,1920]}"
227            )?,
228            PostMsgMetadata {
229                size: 5_342_765,
230                viewtype: Viewtype::Image,
231                filename: "vacation.png".to_string(),
232                wh: Some((1080, 1920)),
233                duration: None,
234            }
235        );
236        assert_eq!(
237            serde_json::from_str::<PostMsgMetadata>(
238                "{\"size\":5000,\"viewtype\":\"Audio\",\"filename\":\"audio-DD-MM-YY.ogg\",\"duration\":152310}"
239            )?,
240            PostMsgMetadata {
241                size: 5_000,
242                viewtype: Viewtype::Audio,
243                filename: "audio-DD-MM-YY.ogg".to_string(),
244                wh: None,
245                duration: Some(152_310),
246            }
247        );
248
249        Ok(())
250    }
251}