1use crate::appstate_sync::Mutation;
8use crate::client::Client;
9use anyhow::Result;
10use chrono::DateTime;
11use log::debug;
12use wacore::appstate::patch_decode::WAPatchName;
13use wacore::types::events::{
14 ArchiveUpdate, ContactUpdate, DeleteChatUpdate, DeleteMessageForMeUpdate, Event,
15 MarkChatAsReadUpdate, MuteUpdate, PinUpdate, StarUpdate,
16};
17use wacore_binary::jid::{Jid, JidExt};
18use waproto::whatsapp as wa;
19
20const MUTE_INDEFINITE: i64 = -1;
22
23pub type SyncActionMessageRange = wa::sync_action_value::SyncActionMessageRange;
24
25pub fn message_range(
28 last_message_timestamp: i64,
29 last_system_message_timestamp: Option<i64>,
30 messages: Vec<(wa::MessageKey, i64)>,
31) -> SyncActionMessageRange {
32 SyncActionMessageRange {
33 last_message_timestamp: Some(last_message_timestamp),
34 last_system_message_timestamp,
35 messages: messages
36 .into_iter()
37 .map(|(key, ts)| wa::sync_action_value::SyncActionMessage {
38 key: Some(key),
39 timestamp: Some(ts),
40 })
41 .collect(),
42 }
43}
44
45pub fn message_key(
46 id: impl Into<String>,
47 remote_jid: &Jid,
48 from_me: bool,
49 participant: Option<&Jid>,
50) -> wa::MessageKey {
51 wa::MessageKey {
52 id: Some(id.into()),
53 remote_jid: Some(remote_jid.to_string()),
54 from_me: Some(from_me),
55 participant: participant.map(|j| j.to_string()),
56 }
57}
58
59pub(crate) fn dispatch_chat_mutation(
61 event_bus: &wacore::types::events::CoreEventBus,
62 m: &Mutation,
63 full_sync: bool,
64) -> bool {
65 if m.operation != wa::syncd_mutation::SyncdOperation::Set || m.index.is_empty() {
66 return false;
67 }
68
69 let kind = &m.index[0];
70
71 if !matches!(
72 kind.as_str(),
73 "mute"
74 | "pin"
75 | "pin_v1"
76 | "archive"
77 | "star"
78 | "contact"
79 | "mark_chat_as_read"
80 | "markChatAsRead"
81 | "deleteChat"
82 | "deleteMessageForMe"
83 ) {
84 return false;
85 }
86
87 let ts = m
88 .action_value
89 .as_ref()
90 .and_then(|v| v.timestamp)
91 .unwrap_or(0);
92 let time = DateTime::from_timestamp_millis(ts).unwrap_or_else(wacore::time::now_utc);
93 let jid: Jid = if m.index.len() > 1 {
94 match m.index[1].parse() {
95 Ok(j) => j,
96 Err(_) => {
97 log::warn!(
98 "Skipping chat mutation '{}': malformed JID '{}'",
99 kind,
100 m.index[1]
101 );
102 return true;
103 }
104 }
105 } else {
106 log::warn!("Skipping chat mutation '{}': missing JID in index", kind);
107 return true;
108 };
109
110 match kind.as_str() {
111 "mute" => {
112 if let Some(val) = &m.action_value
113 && let Some(act) = &val.mute_action
114 {
115 event_bus.dispatch(&Event::MuteUpdate(MuteUpdate {
116 jid,
117 timestamp: time,
118 action: Box::new(*act),
119 from_full_sync: full_sync,
120 }));
121 }
122 true
123 }
124 "pin" | "pin_v1" => {
125 if let Some(val) = &m.action_value
126 && let Some(act) = &val.pin_action
127 {
128 event_bus.dispatch(&Event::PinUpdate(PinUpdate {
129 jid,
130 timestamp: time,
131 action: Box::new(*act),
132 from_full_sync: full_sync,
133 }));
134 }
135 true
136 }
137 "archive" => {
138 if let Some(val) = &m.action_value
139 && let Some(act) = &val.archive_chat_action
140 {
141 event_bus.dispatch(&Event::ArchiveUpdate(ArchiveUpdate {
142 jid,
143 timestamp: time,
144 action: Box::new(act.clone()),
145 from_full_sync: full_sync,
146 }));
147 }
148 true
149 }
150 "star" => {
151 if let Some(val) = &m.action_value
152 && let Some(act) = &val.star_action
153 && let Some((message_id, from_me, participant_jid)) =
154 parse_message_key_fields(kind, &m.index)
155 {
156 event_bus.dispatch(&Event::StarUpdate(StarUpdate {
157 chat_jid: jid,
158 participant_jid,
159 message_id,
160 from_me,
161 timestamp: time,
162 action: Box::new(*act),
163 from_full_sync: full_sync,
164 }));
165 }
166 true
167 }
168 "contact" => {
169 if let Some(val) = &m.action_value
170 && let Some(act) = &val.contact_action
171 {
172 event_bus.dispatch(&Event::ContactUpdate(ContactUpdate {
173 jid,
174 timestamp: time,
175 action: Box::new(act.clone()),
176 from_full_sync: full_sync,
177 }));
178 }
179 true
180 }
181 "mark_chat_as_read" | "markChatAsRead" => {
182 if let Some(val) = &m.action_value
183 && let Some(act) = &val.mark_chat_as_read_action
184 {
185 event_bus.dispatch(&Event::MarkChatAsReadUpdate(MarkChatAsReadUpdate {
186 jid,
187 timestamp: time,
188 action: Box::new(act.clone()),
189 from_full_sync: full_sync,
190 }));
191 }
192 true
193 }
194 "deleteChat" => {
195 if let Some(val) = &m.action_value
196 && let Some(act) = &val.delete_chat_action
197 {
198 let delete_media = m.index.get(2).is_none_or(|v| v != "0");
200 event_bus.dispatch(&Event::DeleteChatUpdate(DeleteChatUpdate {
201 jid,
202 delete_media,
203 timestamp: time,
204 action: Box::new(act.clone()),
205 from_full_sync: full_sync,
206 }));
207 }
208 true
209 }
210 "deleteMessageForMe" => {
211 if let Some(val) = &m.action_value
212 && let Some(act) = &val.delete_message_for_me_action
213 && let Some((message_id, from_me, participant_jid)) =
214 parse_message_key_fields(kind, &m.index)
215 {
216 event_bus.dispatch(&Event::DeleteMessageForMeUpdate(DeleteMessageForMeUpdate {
217 chat_jid: jid,
218 participant_jid,
219 message_id,
220 from_me,
221 timestamp: time,
222 action: Box::new(*act),
223 from_full_sync: full_sync,
224 }));
225 }
226 true
227 }
228 _ => false,
229 }
230}
231
232fn parse_message_key_fields(kind: &str, index: &[String]) -> Option<(String, bool, Option<Jid>)> {
235 if index.len() < 5 {
236 log::warn!(
237 "Skipping {kind} mutation: expected 5 index elements, got {}",
238 index.len()
239 );
240 return None;
241 }
242 let message_id = index[2].clone();
243 let from_me = index[3] == "1";
244 let participant_jid = if index[4] != "0" {
245 match index[4].parse() {
246 Ok(j) => Some(j),
247 Err(_) => {
248 log::warn!(
249 "Skipping {kind} mutation: malformed participant JID '{}'",
250 index[4]
251 );
252 return None;
253 }
254 }
255 } else {
256 None
257 };
258 Some((message_id, from_me, participant_jid))
259}
260
261fn build_message_key_index(
263 action: &str,
264 chat_jid: &Jid,
265 participant_jid: Option<&Jid>,
266 message_id: &str,
267 from_me: bool,
268) -> Result<Vec<u8>> {
269 if chat_jid.is_group() && !from_me && participant_jid.is_none() {
271 anyhow::bail!(
272 "participant_jid is required for group messages not sent by us (action: {action})"
273 );
274 }
275 let from_me_str = if from_me { "1" } else { "0" };
276 let participant = participant_jid
277 .map(|j| j.to_string())
278 .unwrap_or_else(|| "0".to_string());
279 Ok(serde_json::to_vec(&[
280 action,
281 &chat_jid.to_string(),
282 message_id,
283 from_me_str,
284 &participant,
285 ])?)
286}
287
288pub struct ChatActions<'a> {
290 client: &'a Client,
291}
292
293impl<'a> ChatActions<'a> {
294 pub(crate) fn new(client: &'a Client) -> Self {
295 Self { client }
296 }
297
298 pub async fn archive_chat(
299 &self,
300 jid: &Jid,
301 message_range: Option<SyncActionMessageRange>,
302 ) -> Result<()> {
303 debug!("Archiving chat {jid}");
304 self.send_archive_mutation(jid, true, message_range).await
305 }
306
307 pub async fn unarchive_chat(
308 &self,
309 jid: &Jid,
310 message_range: Option<SyncActionMessageRange>,
311 ) -> Result<()> {
312 debug!("Unarchiving chat {jid}");
313 self.send_archive_mutation(jid, false, message_range).await
314 }
315
316 pub async fn pin_chat(&self, jid: &Jid) -> Result<()> {
317 debug!("Pinning chat {jid}");
318 self.send_pin_mutation(jid, true).await
319 }
320
321 pub async fn unpin_chat(&self, jid: &Jid) -> Result<()> {
322 debug!("Unpinning chat {jid}");
323 self.send_pin_mutation(jid, false).await
324 }
325
326 pub async fn mute_chat(&self, jid: &Jid) -> Result<()> {
327 debug!("Muting chat {jid} indefinitely");
328 self.send_mute_mutation(jid, true, MUTE_INDEFINITE).await
329 }
330
331 pub async fn mute_chat_until(&self, jid: &Jid, mute_end_timestamp_ms: i64) -> Result<()> {
333 if mute_end_timestamp_ms <= 0 {
334 anyhow::bail!(
335 "mute_end_timestamp_ms must be a positive future timestamp (use mute_chat() for indefinite)"
336 );
337 }
338 let now_ms = wacore::time::now_millis();
339 if mute_end_timestamp_ms <= now_ms {
340 anyhow::bail!(
341 "mute_end_timestamp_ms is in the past ({mute_end_timestamp_ms} <= {now_ms})"
342 );
343 }
344 debug!("Muting chat {jid} until {mute_end_timestamp_ms}");
345 self.send_mute_mutation(jid, true, mute_end_timestamp_ms)
346 .await
347 }
348
349 pub async fn unmute_chat(&self, jid: &Jid) -> Result<()> {
350 debug!("Unmuting chat {jid}");
351 self.send_mute_mutation(jid, false, 0).await
352 }
353
354 pub async fn star_message(
356 &self,
357 chat_jid: &Jid,
358 participant_jid: Option<&Jid>,
359 message_id: &str,
360 from_me: bool,
361 ) -> Result<()> {
362 debug!("Starring message {message_id} in {chat_jid}");
363 self.send_star_mutation(chat_jid, participant_jid, message_id, from_me, true)
364 .await
365 }
366
367 pub async fn unstar_message(
368 &self,
369 chat_jid: &Jid,
370 participant_jid: Option<&Jid>,
371 message_id: &str,
372 from_me: bool,
373 ) -> Result<()> {
374 debug!("Unstarring message {message_id} in {chat_jid}");
375 self.send_star_mutation(chat_jid, participant_jid, message_id, from_me, false)
376 .await
377 }
378
379 pub async fn mark_chat_as_read(
381 &self,
382 jid: &Jid,
383 read: bool,
384 message_range: Option<SyncActionMessageRange>,
385 ) -> Result<()> {
386 debug!(
387 "Marking chat {jid} as {}",
388 if read { "read" } else { "unread" }
389 );
390 let index = serde_json::to_vec(&["markChatAsRead", &jid.to_string()])?;
391 let value = wa::SyncActionValue {
392 mark_chat_as_read_action: Some(wa::sync_action_value::MarkChatAsReadAction {
393 read: Some(read),
394 message_range,
395 }),
396 timestamp: Some(wacore::time::now_millis()),
397 ..Default::default()
398 };
399 self.send_mutation(WAPatchName::RegularLow, &index, &value)
400 .await
401 }
402
403 pub async fn delete_chat(
404 &self,
405 jid: &Jid,
406 delete_media: bool,
407 message_range: Option<SyncActionMessageRange>,
408 ) -> Result<()> {
409 debug!("Deleting chat {jid}");
410 let delete_media_str = if delete_media { "1" } else { "0" };
411 let index = serde_json::to_vec(&["deleteChat", &jid.to_string(), delete_media_str])?;
412 let value = wa::SyncActionValue {
413 delete_chat_action: Some(wa::sync_action_value::DeleteChatAction { message_range }),
414 timestamp: Some(wacore::time::now_millis()),
415 ..Default::default()
416 };
417 self.send_mutation(WAPatchName::RegularHigh, &index, &value)
418 .await
419 }
420
421 pub async fn delete_message_for_me(
424 &self,
425 chat_jid: &Jid,
426 participant_jid: Option<&Jid>,
427 message_id: &str,
428 from_me: bool,
429 delete_media: bool,
430 message_timestamp: Option<i64>,
431 ) -> Result<()> {
432 debug!("Deleting message {message_id} for me in {chat_jid}");
433 let index = build_message_key_index(
434 "deleteMessageForMe",
435 chat_jid,
436 participant_jid,
437 message_id,
438 from_me,
439 )?;
440 let value = wa::SyncActionValue {
441 delete_message_for_me_action: Some(wa::sync_action_value::DeleteMessageForMeAction {
442 delete_media: Some(delete_media),
443 message_timestamp,
444 }),
445 timestamp: Some(wacore::time::now_millis()),
446 ..Default::default()
447 };
448 self.send_mutation(WAPatchName::RegularHigh, &index, &value)
449 .await
450 }
451
452 async fn send_archive_mutation(
453 &self,
454 jid: &Jid,
455 archived: bool,
456 message_range: Option<SyncActionMessageRange>,
457 ) -> Result<()> {
458 let index = serde_json::to_vec(&["archive", &jid.to_string()])?;
459 let value = wa::SyncActionValue {
460 archive_chat_action: Some(wa::sync_action_value::ArchiveChatAction {
461 archived: Some(archived),
462 message_range,
463 }),
464 timestamp: Some(wacore::time::now_millis()),
465 ..Default::default()
466 };
467 self.send_mutation(WAPatchName::RegularLow, &index, &value)
468 .await
469 }
470
471 async fn send_pin_mutation(&self, jid: &Jid, pinned: bool) -> Result<()> {
472 let index = serde_json::to_vec(&["pin_v1", &jid.to_string()])?;
473 let value = wa::SyncActionValue {
474 pin_action: Some(wa::sync_action_value::PinAction {
475 pinned: Some(pinned),
476 }),
477 timestamp: Some(wacore::time::now_millis()),
478 ..Default::default()
479 };
480 self.send_mutation(WAPatchName::RegularLow, &index, &value)
481 .await
482 }
483
484 async fn send_mute_mutation(
485 &self,
486 jid: &Jid,
487 muted: bool,
488 mute_end_timestamp_ms: i64,
489 ) -> Result<()> {
490 let index = serde_json::to_vec(&["mute", &jid.to_string()])?;
491 let mute_end = if muted {
493 Some(mute_end_timestamp_ms)
494 } else {
495 Some(0)
496 };
497 let value = wa::SyncActionValue {
498 mute_action: Some(wa::sync_action_value::MuteAction {
499 muted: Some(muted),
500 mute_end_timestamp: mute_end,
501 auto_muted: None,
502 }),
503 timestamp: Some(wacore::time::now_millis()),
504 ..Default::default()
505 };
506 self.send_mutation(WAPatchName::RegularHigh, &index, &value)
507 .await
508 }
509
510 async fn send_star_mutation(
511 &self,
512 chat_jid: &Jid,
513 participant_jid: Option<&Jid>,
514 message_id: &str,
515 from_me: bool,
516 starred: bool,
517 ) -> Result<()> {
518 let index =
519 build_message_key_index("star", chat_jid, participant_jid, message_id, from_me)?;
520 let value = wa::SyncActionValue {
521 star_action: Some(wa::sync_action_value::StarAction {
522 starred: Some(starred),
523 }),
524 timestamp: Some(wacore::time::now_millis()),
525 ..Default::default()
526 };
527 self.send_mutation(WAPatchName::RegularHigh, &index, &value)
528 .await
529 }
530
531 async fn send_mutation(
532 &self,
533 collection: WAPatchName,
534 index: &[u8],
535 value: &wa::SyncActionValue,
536 ) -> Result<()> {
537 use rand::Rng;
538 use wacore::appstate::encode::encode_record;
539
540 let proc = self.client.get_app_state_processor().await;
541 let key_id = proc
542 .backend
543 .get_latest_sync_key_id()
544 .await
545 .map_err(|e| anyhow::anyhow!(e))?
546 .ok_or_else(|| anyhow::anyhow!("No app state sync key available"))?;
547 let keys = proc.get_app_state_key(&key_id).await?;
548
549 let mut iv = [0u8; 16];
550 rand::make_rng::<rand::rngs::StdRng>().fill_bytes(&mut iv);
551
552 let (mutation, value_mac) = encode_record(
553 wa::syncd_mutation::SyncdOperation::Set,
554 index,
555 value,
556 &keys,
557 &key_id,
558 &iv,
559 );
560
561 self.client
562 .send_app_state_patch(collection.as_str(), vec![(mutation, value_mac)])
563 .await
564 }
565}
566
567impl Client {
568 pub fn chat_actions(&self) -> ChatActions<'_> {
569 ChatActions::new(self)
570 }
571}