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