1use serde_json::{Map, Value, json};
14
15use imessage_core::typedstream;
16use imessage_db::imessage::entities::Message;
17
18use crate::attachment::serialize_attachments;
19use crate::chat::serialize_chats;
20use crate::config::{AttachmentSerializerConfig, ChatSerializerConfig, MessageSerializerConfig};
21use crate::handle::serialize_handle;
22use crate::plist_decode;
23
24pub fn serialize_message(
26 message: &Message,
27 config: &MessageSerializerConfig,
28 attachment_config: &AttachmentSerializerConfig,
29 is_for_notification: bool,
30) -> Value {
31 let mut map = Map::new();
32
33 map.insert("originalROWID".to_string(), json!(message.rowid));
35 map.insert("guid".to_string(), json!(message.guid));
36
37 let text = match &message.text {
41 Some(t) if !t.is_empty() => Some(t.clone()),
42 _ => message
43 .attributed_body
44 .as_deref()
45 .and_then(typedstream::extract_text),
46 };
47 map.insert("text".to_string(), json!(text));
48
49 let attributed_body = message
51 .attributed_body
52 .as_deref()
53 .and_then(typedstream::decode_attributed_body);
54 map.insert(
55 "attributedBody".to_string(),
56 attributed_body.unwrap_or(Value::Null),
57 );
58
59 let handle_json = message.handle.as_ref().map(|h| serialize_handle(h, false));
61 map.insert("handle".to_string(), handle_json.unwrap_or(Value::Null));
62
63 map.insert("handleId".to_string(), json!(message.handle_id));
64 map.insert("otherHandle".to_string(), json!(message.other_handle));
65
66 map.insert(
68 "attachments".to_string(),
69 serialize_attachments(&message.attachments, attachment_config, is_for_notification),
70 );
71
72 map.insert("subject".to_string(), json!(message.subject));
73 map.insert("error".to_string(), json!(message.error));
74 map.insert("dateCreated".to_string(), json!(message.date));
75 map.insert("dateRead".to_string(), json!(message.date_read));
76 map.insert("dateDelivered".to_string(), json!(message.date_delivered));
77 map.insert("isDelivered".to_string(), json!(message.is_delivered));
78 map.insert("isFromMe".to_string(), json!(message.is_from_me));
79 map.insert("hasDdResults".to_string(), json!(message.has_dd_results));
80 map.insert("isArchived".to_string(), json!(message.is_archive));
81 map.insert("itemType".to_string(), json!(message.item_type));
82 map.insert("groupTitle".to_string(), json!(message.group_title));
83 map.insert(
84 "groupActionType".to_string(),
85 json!(message.group_action_type),
86 );
87 map.insert(
88 "balloonBundleId".to_string(),
89 json!(message.balloon_bundle_id),
90 );
91 map.insert(
92 "associatedMessageGuid".to_string(),
93 json!(message.associated_message_guid),
94 );
95 map.insert(
96 "associatedMessageType".to_string(),
97 json!(message.associated_message_type),
98 );
99 map.insert(
100 "associatedMessageEmoji".to_string(),
101 json!(message.associated_message_emoji),
102 );
103 map.insert(
104 "expressiveSendStyleId".to_string(),
105 json!(message.expressive_send_style_id),
106 );
107 map.insert(
108 "threadOriginatorGuid".to_string(),
109 json!(message.thread_originator_guid),
110 );
111
112 let has_payload = message.payload_data.is_some();
114 map.insert("hasPayloadData".to_string(), json!(has_payload));
115
116 if !is_for_notification {
118 map.insert("country".to_string(), json!(message.country));
119 map.insert("isDelayed".to_string(), json!(message.is_delayed));
120 map.insert("isAutoReply".to_string(), json!(message.is_auto_reply));
121 map.insert(
122 "isSystemMessage".to_string(),
123 json!(message.is_system_message),
124 );
125 map.insert(
126 "isServiceMessage".to_string(),
127 json!(message.is_service_message),
128 );
129 map.insert("isForward".to_string(), json!(message.is_forward));
130 map.insert(
131 "threadOriginatorPart".to_string(),
132 json!(message.thread_originator_part),
133 );
134 map.insert(
135 "isCorrupt".to_string(),
136 json!(message.is_corrupt.unwrap_or(false)),
137 );
138 map.insert("datePlayed".to_string(), json!(message.date_played));
139 map.insert("cacheRoomnames".to_string(), json!(message.cache_roomnames));
140 map.insert(
141 "isSpam".to_string(),
142 json!(message.is_spam.unwrap_or(false)),
143 );
144
145 map.insert("isExpired".to_string(), json!(message.is_expirable));
147
148 map.insert(
149 "timeExpressiveSendPlayed".to_string(),
150 json!(message.time_expressive_send_played),
151 );
152 map.insert(
153 "isAudioMessage".to_string(),
154 json!(message.is_audio_message),
155 );
156 map.insert("replyToGuid".to_string(), json!(message.reply_to_guid));
157 map.insert("shareStatus".to_string(), json!(message.share_status));
158 map.insert("shareDirection".to_string(), json!(message.share_direction));
159
160 map.insert(
161 "wasDeliveredQuietly".to_string(),
162 json!(message.was_delivered_quietly.unwrap_or(false)),
163 );
164 map.insert(
165 "didNotifyRecipient".to_string(),
166 json!(message.did_notify_recipient.unwrap_or(false)),
167 );
168 }
169
170 if config.include_chats {
172 let chat_config = ChatSerializerConfig {
173 include_participants: false,
174 include_messages: false,
175 };
176 map.insert(
177 "chats".to_string(),
178 serialize_chats(&message.chats, &chat_config, is_for_notification),
179 );
180 }
181
182 let msg_summary = message
184 .message_summary_info
185 .as_deref()
186 .and_then(plist_decode::decode_message_plist);
187 map.insert(
188 "messageSummaryInfo".to_string(),
189 msg_summary.unwrap_or(Value::Null),
190 );
191
192 let payload = message
194 .payload_data
195 .as_deref()
196 .and_then(plist_decode::decode_message_plist);
197 map.insert("payloadData".to_string(), payload.unwrap_or(Value::Null));
198
199 map.insert("dateEdited".to_string(), json!(message.date_edited));
200 map.insert("dateRetracted".to_string(), json!(message.date_retracted));
201 map.insert("partCount".to_string(), json!(message.part_count));
202
203 if !config.parse_attributed_body
205 && let Some(v) = map.get_mut("attributedBody")
206 {
207 *v = Value::Null;
208 }
209 if !config.parse_message_summary
210 && let Some(v) = map.get_mut("messageSummaryInfo")
211 {
212 *v = Value::Null;
213 }
214 if !config.parse_payload_data
215 && let Some(v) = map.get_mut("payloadData")
216 {
217 *v = Value::Null;
218 }
219
220 Value::Object(map)
221}
222
223pub fn serialize_messages(
225 messages: &[Message],
226 config: &MessageSerializerConfig,
227 attachment_config: &AttachmentSerializerConfig,
228 is_for_notification: bool,
229) -> Value {
230 let list: Vec<Value> = messages
231 .iter()
232 .map(|m| serialize_message(m, config, attachment_config, is_for_notification))
233 .collect();
234 Value::Array(list)
235}
236
237#[cfg(test)]
238mod tests {
239 use super::*;
240 use imessage_db::imessage::entities::Handle;
241
242 fn test_message() -> Message {
243 Message {
244 rowid: 100,
245 guid: "MSG-GUID-1234".to_string(),
246 text: Some("Hello, world!".to_string()),
247 handle_id: 1,
248 handle: Some(Handle {
249 rowid: 1,
250 id: "+15551234567".to_string(),
251 country: Some("us".to_string()),
252 service: "iMessage".to_string(),
253 uncanonicalized_id: None,
254 }),
255 date: Some(1700000000000),
256 date_read: None,
257 date_delivered: Some(1700000001000),
258 is_from_me: false,
259 is_delivered: true,
260 is_archive: false,
261 item_type: 0,
262 group_action_type: 0,
263 has_dd_results: false,
264 error: 0,
265 ..Default::default()
266 }
267 }
268
269 #[test]
270 fn serialize_basic_message() {
271 let config = MessageSerializerConfig {
272 include_chats: false,
273 ..Default::default()
274 };
275 let att_config = AttachmentSerializerConfig::default();
276 let json = serialize_message(&test_message(), &config, &att_config, false);
277
278 assert_eq!(json["originalROWID"], 100);
279 assert_eq!(json["guid"], "MSG-GUID-1234");
280 assert_eq!(json["text"], "Hello, world!");
281 assert_eq!(json["handleId"], 1);
282 assert_eq!(json["isFromMe"], false);
283 assert_eq!(json["isDelivered"], true);
284 assert_eq!(json["dateCreated"], 1700000000000i64);
285 assert_eq!(json["dateDelivered"], 1700000001000i64);
286 assert!(json["dateRead"].is_null());
287 }
288
289 #[test]
290 fn handle_serialized_inline() {
291 let config = MessageSerializerConfig::default();
292 let att_config = AttachmentSerializerConfig::default();
293 let json = serialize_message(&test_message(), &config, &att_config, false);
294
295 assert!(json["handle"].is_object());
296 assert_eq!(json["handle"]["address"], "+15551234567");
297 assert_eq!(json["handle"]["service"], "iMessage");
298 }
299
300 #[test]
301 fn null_handle_when_missing() {
302 let mut msg = test_message();
303 msg.handle = None;
304 let config = MessageSerializerConfig::default();
305 let att_config = AttachmentSerializerConfig::default();
306 let json = serialize_message(&msg, &config, &att_config, false);
307 assert!(json["handle"].is_null());
308 }
309
310 #[test]
311 fn notification_excludes_non_essential_fields() {
312 let config = MessageSerializerConfig::default();
313 let att_config = AttachmentSerializerConfig::default();
314 let json = serialize_message(&test_message(), &config, &att_config, true);
315
316 assert!(json.get("originalROWID").is_some());
318 assert!(json.get("guid").is_some());
319 assert!(json.get("text").is_some());
320
321 assert!(json.get("country").is_none());
323 assert!(json.get("isDelayed").is_none());
324 assert!(json.get("isAutoReply").is_none());
325 assert!(json.get("shareStatus").is_none());
326 }
327
328 #[test]
329 fn is_expired_maps_to_is_expirable() {
330 let mut msg = test_message();
331 msg.is_expirable = true;
332 let config = MessageSerializerConfig::default();
333 let att_config = AttachmentSerializerConfig::default();
334 let json = serialize_message(&msg, &config, &att_config, false);
335 assert_eq!(json["isExpired"], true);
336 }
337
338 #[test]
339 fn has_payload_data_flag() {
340 let mut msg = test_message();
341 msg.payload_data = Some(vec![1, 2, 3]);
342 let config = MessageSerializerConfig::default();
343 let att_config = AttachmentSerializerConfig::default();
344 let json = serialize_message(&msg, &config, &att_config, false);
345 assert_eq!(json["hasPayloadData"], true);
346
347 msg.payload_data = None;
348 let json = serialize_message(&msg, &config, &att_config, false);
349 assert_eq!(json["hasPayloadData"], false);
350 }
351
352 #[test]
353 fn empty_attachments_array() {
354 let config = MessageSerializerConfig::default();
355 let att_config = AttachmentSerializerConfig::default();
356 let json = serialize_message(&test_message(), &config, &att_config, false);
357 assert!(json["attachments"].is_array());
358 assert_eq!(json["attachments"].as_array().unwrap().len(), 0);
359 }
360
361 #[test]
362 fn field_order_core() {
363 let config = MessageSerializerConfig {
364 include_chats: false,
365 ..Default::default()
366 };
367 let att_config = AttachmentSerializerConfig::default();
368 let json = serialize_message(&test_message(), &config, &att_config, true);
369 let serialized = serde_json::to_string(&json).unwrap();
370
371 let rowid_pos = serialized.find("originalROWID").unwrap();
373 let guid_pos = serialized.find("\"guid\"").unwrap();
374 let text_pos = serialized.find("\"text\"").unwrap();
375 let handle_pos = serialized.find("\"handle\"").unwrap();
376 let date_pos = serialized.find("dateCreated").unwrap();
377
378 assert!(rowid_pos < guid_pos);
379 assert!(guid_pos < text_pos);
380 assert!(text_pos < handle_pos);
381 assert!(handle_pos < date_pos);
382 }
383}