1use serde_json::{Value, json};
5
6use crate::transaction::TransactionType;
7
8pub struct Action {
10 pub name: &'static str,
11 pub data: Value,
12 pub transaction_type: Option<TransactionType>,
13}
14
15#[derive(Debug, Default)]
17pub struct SendOptions<'a> {
18 pub subject: Option<&'a str>,
19 pub effect_id: Option<&'a str>,
20 pub selected_message_guid: Option<&'a str>,
21 pub part_index: Option<i64>,
22 pub attributed_body: Option<&'a Value>,
23}
24
25fn apply_send_options(data: &mut Value, opts: &SendOptions) {
27 if let Some(s) = opts.subject {
28 data["subject"] = json!(s);
29 }
30 if let Some(e) = opts.effect_id {
31 data["effectId"] = json!(e);
32 }
33 if let Some(g) = opts.selected_message_guid {
34 data["selectedMessageGuid"] = json!(g);
35 }
36 if let Some(ab) = opts.attributed_body {
37 data["attributedBody"] = ab.clone();
38 }
39}
40
41pub fn send_message(
46 chat_guid: &str,
47 message: &str,
48 opts: &SendOptions,
49 text_formatting: Option<&Value>,
50 dd_scan: Option<bool>,
51) -> Action {
52 let mut data = json!({
54 "chatGuid": chat_guid,
55 "message": message,
56 "subject": Value::Null,
57 "attributedBody": Value::Null,
58 "effectId": Value::Null,
59 "selectedMessageGuid": Value::Null,
60 "partIndex": opts.part_index.unwrap_or(0),
61 "textFormatting": Value::Null,
62 });
63 apply_send_options(&mut data, opts);
64 if let Some(tf) = text_formatting {
65 data["textFormatting"] = tf.clone();
66 }
67 if let Some(dd) = dd_scan {
68 data["ddScan"] = json!(if dd { 1 } else { 0 });
69 }
70
71 Action {
72 name: "send-message",
73 data,
74 transaction_type: Some(TransactionType::Message),
75 }
76}
77
78pub fn send_multipart(
79 chat_guid: &str,
80 parts: &Value,
81 opts: &SendOptions,
82 dd_scan: Option<bool>,
83) -> Action {
84 let mut data = json!({
86 "chatGuid": chat_guid,
87 "parts": parts,
88 "subject": Value::Null,
89 "effectId": Value::Null,
90 "selectedMessageGuid": Value::Null,
91 "partIndex": opts.part_index.unwrap_or(0),
92 "attributedBody": Value::Null,
93 });
94 apply_send_options(&mut data, opts);
95 if let Some(dd) = dd_scan {
96 data["ddScan"] = json!(if dd { 1 } else { 0 });
97 }
98
99 Action {
100 name: "send-multipart",
101 data,
102 transaction_type: Some(TransactionType::Message),
103 }
104}
105
106pub fn send_reaction(
107 chat_guid: &str,
108 selected_message_guid: &str,
109 reaction_type: &str,
110 part_index: Option<i64>,
111 emoji: Option<&str>,
112 sticker_path: Option<&str>,
113) -> Action {
114 let mut data = json!({
116 "chatGuid": chat_guid,
117 "selectedMessageGuid": selected_message_guid,
118 "reactionType": reaction_type,
119 "partIndex": part_index.unwrap_or(0),
120 });
121 if let Some(e) = emoji {
122 data["emoji"] = json!(e);
123 }
124 if let Some(sp) = sticker_path {
125 data["stickerPath"] = json!(sp);
126 }
127
128 Action {
129 name: "send-reaction",
130 data,
131 transaction_type: Some(TransactionType::Message),
132 }
133}
134
135pub fn edit_message(
136 chat_guid: &str,
137 message_guid: &str,
138 edited_message: &str,
139 backwards_compat_message: &str,
140 part_index: Option<i64>,
141) -> Action {
142 let data = json!({
144 "chatGuid": chat_guid,
145 "messageGuid": message_guid,
146 "editedMessage": edited_message,
147 "backwardsCompatibilityMessage": backwards_compat_message,
148 "partIndex": part_index.unwrap_or(0),
149 });
150
151 Action {
152 name: "edit-message",
153 data,
154 transaction_type: Some(TransactionType::Message),
155 }
156}
157
158pub fn unsend_message(chat_guid: &str, message_guid: &str, part_index: Option<i64>) -> Action {
159 let data = json!({
161 "chatGuid": chat_guid,
162 "messageGuid": message_guid,
163 "partIndex": part_index.unwrap_or(0),
164 });
165
166 Action {
167 name: "unsend-message",
168 data,
169 transaction_type: Some(TransactionType::Message),
170 }
171}
172
173pub fn get_embedded_media(chat_guid: &str, message_guid: &str) -> Action {
174 Action {
175 name: "balloon-bundle-media-path",
176 data: json!({
177 "chatGuid": chat_guid,
178 "messageGuid": message_guid,
179 }),
180 transaction_type: Some(TransactionType::Message),
181 }
182}
183
184pub fn notify_silenced(chat_guid: &str, message_guid: &str) -> Action {
185 Action {
186 name: "notify-anyways",
187 data: json!({
188 "chatGuid": chat_guid,
189 "messageGuid": message_guid,
190 }),
191 transaction_type: Some(TransactionType::Message),
192 }
193}
194
195pub fn search_messages(query: &str, match_type: &str) -> Action {
196 Action {
197 name: "search-messages",
198 data: json!({
199 "query": query,
200 "matchType": match_type,
201 }),
202 transaction_type: Some(TransactionType::Message),
203 }
204}
205
206pub fn create_chat(
211 addresses: &[String],
212 message: &str,
213 service: &str,
214 attributed_body: Option<&Value>,
215 effect_id: Option<&str>,
216 subject: Option<&str>,
217) -> Action {
218 let mut data = json!({
219 "addresses": addresses,
220 "message": message,
221 "service": service,
222 });
223 if let Some(ab) = attributed_body {
224 data["attributedBody"] = ab.clone();
225 }
226 if let Some(e) = effect_id {
227 data["effectId"] = json!(e);
228 }
229 if let Some(s) = subject {
230 data["subject"] = json!(s);
231 }
232
233 Action {
234 name: "create-chat",
235 data,
236 transaction_type: Some(TransactionType::Message), }
238}
239
240pub fn delete_message(chat_guid: &str, message_guid: &str) -> Action {
241 Action {
242 name: "delete-message",
243 data: json!({
244 "chatGuid": chat_guid,
245 "messageGuid": message_guid,
246 }),
247 transaction_type: Some(TransactionType::Chat),
248 }
249}
250
251pub fn start_typing(chat_guid: &str) -> Action {
252 Action {
253 name: "start-typing",
254 data: json!({ "chatGuid": chat_guid }),
255 transaction_type: None, }
257}
258
259pub fn stop_typing(chat_guid: &str) -> Action {
260 Action {
261 name: "stop-typing",
262 data: json!({ "chatGuid": chat_guid }),
263 transaction_type: None,
264 }
265}
266
267pub fn mark_chat_read(chat_guid: &str) -> Action {
268 Action {
269 name: "mark-chat-read",
270 data: json!({ "chatGuid": chat_guid }),
271 transaction_type: None,
272 }
273}
274
275pub fn mark_chat_unread(chat_guid: &str) -> Action {
276 Action {
277 name: "mark-chat-unread",
278 data: json!({ "chatGuid": chat_guid }),
279 transaction_type: None,
280 }
281}
282
283pub fn add_participant(chat_guid: &str, address: &str) -> Action {
284 Action {
285 name: "add-participant",
286 data: json!({
287 "chatGuid": chat_guid,
288 "address": address,
289 }),
290 transaction_type: Some(TransactionType::Chat),
291 }
292}
293
294pub fn remove_participant(chat_guid: &str, address: &str) -> Action {
295 Action {
296 name: "remove-participant",
297 data: json!({
298 "chatGuid": chat_guid,
299 "address": address,
300 }),
301 transaction_type: Some(TransactionType::Chat),
302 }
303}
304
305pub fn set_display_name(chat_guid: &str, new_name: &str) -> Action {
306 Action {
307 name: "set-display-name",
308 data: json!({
309 "chatGuid": chat_guid,
310 "newName": new_name,
311 }),
312 transaction_type: Some(TransactionType::Chat),
313 }
314}
315
316pub fn set_group_chat_icon(chat_guid: &str, file_path: Option<&str>) -> Action {
317 Action {
318 name: "update-group-photo",
319 data: json!({
320 "chatGuid": chat_guid,
321 "filePath": file_path,
322 }),
323 transaction_type: Some(TransactionType::Chat),
324 }
325}
326
327pub fn should_offer_contact_sharing(chat_guid: &str) -> Action {
328 Action {
329 name: "should-offer-nickname-sharing",
330 data: json!({ "chatGuid": chat_guid }),
331 transaction_type: Some(TransactionType::Other),
332 }
333}
334
335pub fn share_contact_card(chat_guid: &str) -> Action {
336 Action {
337 name: "share-nickname",
338 data: json!({ "chatGuid": chat_guid }),
339 transaction_type: None,
340 }
341}
342
343pub fn leave_chat(chat_guid: &str) -> Action {
344 Action {
345 name: "leave-chat",
346 data: json!({ "chatGuid": chat_guid }),
347 transaction_type: Some(TransactionType::Chat),
348 }
349}
350
351pub fn delete_chat(chat_guid: &str) -> Action {
352 Action {
353 name: "delete-chat",
354 data: json!({ "chatGuid": chat_guid }),
355 transaction_type: Some(TransactionType::Chat),
356 }
357}
358
359pub fn get_focus_status(address: &str) -> Action {
364 Action {
365 name: "check-focus-status",
366 data: json!({ "address": address }),
367 transaction_type: Some(TransactionType::Handle),
368 }
369}
370
371pub fn get_imessage_availability(address: &str) -> Action {
372 let alias_type = if address.contains('@') {
373 "email"
374 } else {
375 "phone"
376 };
377 Action {
378 name: "check-imessage-availability",
379 data: json!({
380 "aliasType": alias_type,
381 "address": address,
382 }),
383 transaction_type: Some(TransactionType::Handle),
384 }
385}
386
387pub fn get_facetime_availability(address: &str) -> Action {
388 let alias_type = if address.contains('@') {
389 "email"
390 } else {
391 "phone"
392 };
393 Action {
394 name: "check-facetime-availability",
395 data: json!({
396 "aliasType": alias_type,
397 "address": address,
398 }),
399 transaction_type: Some(TransactionType::Handle),
400 }
401}
402
403pub fn send_attachment(
408 chat_guid: &str,
409 file_path: &str,
410 is_audio_message: bool,
411 opts: &SendOptions,
412) -> Action {
413 let mut data = json!({
414 "chatGuid": chat_guid,
415 "filePath": file_path,
416 "isAudioMessage": if is_audio_message { 1 } else { 0 },
417 "partIndex": opts.part_index.unwrap_or(0),
420 "attributedBody": Value::Null,
421 "subject": Value::Null,
422 "effectId": Value::Null,
423 "selectedMessageGuid": Value::Null,
424 });
425 apply_send_options(&mut data, opts);
426
427 Action {
428 name: "send-attachment",
429 data,
430 transaction_type: Some(TransactionType::Attachment),
431 }
432}
433
434pub fn download_purged_attachment(attachment_guid: &str) -> Action {
435 Action {
436 name: "download-purged-attachment",
437 data: json!({ "attachmentGuid": attachment_guid }),
438 transaction_type: None,
439 }
440}
441
442pub fn refresh_findmy_friends() -> Action {
447 Action {
448 name: "refresh-findmy-friends",
449 data: Value::Null,
450 transaction_type: Some(TransactionType::FindMy),
451 }
452}
453
454pub fn get_findmy_key() -> Action {
455 Action {
456 name: "get-findmy-key",
457 data: Value::Null,
458 transaction_type: Some(TransactionType::FindMy),
459 }
460}
461
462pub fn get_account_info() -> Action {
467 Action {
468 name: "get-account-info",
469 data: Value::Null,
470 transaction_type: Some(TransactionType::Other),
471 }
472}
473
474pub fn get_contact_card(address: &str) -> Action {
475 Action {
476 name: "get-nickname-info",
477 data: json!({ "address": address }),
478 transaction_type: Some(TransactionType::Other),
479 }
480}
481
482pub fn modify_active_alias(alias: &str) -> Action {
483 Action {
484 name: "modify-active-alias",
485 data: json!({ "alias": alias }),
486 transaction_type: Some(TransactionType::Other),
487 }
488}
489
490pub fn answer_call(call_uuid: &str) -> Action {
495 Action {
496 name: "answer-call",
497 data: json!({ "callUUID": call_uuid }),
498 transaction_type: Some(TransactionType::Other),
499 }
500}
501
502pub fn leave_call(call_uuid: &str) -> Action {
503 Action {
504 name: "leave-call",
505 data: json!({ "callUUID": call_uuid }),
506 transaction_type: None,
507 }
508}
509
510pub fn generate_facetime_link(call_uuid: Option<&str>) -> Action {
515 Action {
516 name: "generate-link",
517 data: json!({ "callUUID": call_uuid }),
518 transaction_type: Some(TransactionType::Other),
519 }
520}
521
522pub fn check_typing_status(chat_guid: &str) -> Action {
523 Action {
524 name: "check-typing-status",
525 data: json!({ "chatGuid": chat_guid }),
526 transaction_type: Some(TransactionType::Chat),
527 }
528}
529
530pub fn admit_participant(conversation_uuid: &str, handle_uuid: &str) -> Action {
531 Action {
532 name: "admit-pending-member",
533 data: json!({
534 "conversationUUID": conversation_uuid,
535 "handleUUID": handle_uuid,
536 }),
537 transaction_type: Some(TransactionType::Other),
538 }
539}
540
541#[cfg(test)]
542mod tests {
543 use super::*;
544
545 #[test]
546 fn send_message_action_shape() {
547 let action = send_message(
548 "iMessage;-;+1555",
549 "hello",
550 &SendOptions::default(),
551 None,
552 None,
553 );
554 assert_eq!(action.name, "send-message");
555 assert_eq!(action.data["chatGuid"], "iMessage;-;+1555");
556 assert_eq!(action.data["message"], "hello");
557 assert_eq!(action.transaction_type, Some(TransactionType::Message));
558 }
559
560 #[test]
561 fn create_chat_uses_message_transaction() {
562 let action = create_chat(&["addr".to_string()], "hi", "iMessage", None, None, None);
563 assert_eq!(action.name, "create-chat");
564 assert_eq!(action.transaction_type, Some(TransactionType::Message));
565 }
566
567 #[test]
568 fn fire_and_forget_actions_have_no_transaction() {
569 let a = start_typing("guid");
570 assert!(a.transaction_type.is_none());
571 let b = stop_typing("guid");
572 assert!(b.transaction_type.is_none());
573 let c = mark_chat_read("guid");
574 assert!(c.transaction_type.is_none());
575 }
576
577 #[test]
578 fn imessage_availability_detects_email() {
579 let a = get_imessage_availability("user@icloud.com");
580 assert_eq!(a.data["aliasType"], "email");
581 let b = get_imessage_availability("+15551234567");
582 assert_eq!(b.data["aliasType"], "phone");
583 }
584
585 #[test]
586 fn send_attachment_encodes_audio_as_int() {
587 let a = send_attachment("guid", "/path", true, &SendOptions::default());
588 assert_eq!(a.data["isAudioMessage"], 1);
589 assert_eq!(a.data["partIndex"], 0); let b = send_attachment("guid", "/path", false, &SendOptions::default());
591 assert_eq!(b.data["isAudioMessage"], 0);
592 }
593
594 #[test]
595 fn dd_scan_encodes_as_int() {
596 let a = send_message("g", "m", &SendOptions::default(), None, Some(true));
597 assert_eq!(a.data["ddScan"], 1);
598 }
599}