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#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
13pub struct PostMsgMetadata {
14 pub(crate) size: u64,
16 pub(crate) viewtype: Viewtype,
18 pub(crate) filename: String,
20 #[serde(skip_serializing_if = "Option::is_none")]
22 pub(crate) wh: Option<(i32, i32)>,
23 #[serde(skip_serializing_if = "Option::is_none")]
25 pub(crate) duration: Option<i32>,
26}
27
28impl PostMsgMetadata {
29 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 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 #[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 #[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 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]
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]
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}