1use crate::appstate_sync::Mutation;
18use crate::client::Client;
19use anyhow::Result;
20use chrono::DateTime;
21use log::debug;
22use std::sync::Arc;
23use wacore::appstate::patch_decode::WAPatchName;
24use wacore::types::events::{
25 ArchiveUpdate, ContactUpdate, Event, MarkChatAsReadUpdate, MuteUpdate, PinUpdate, StarUpdate,
26};
27use wacore_binary::jid::{Jid, JidExt};
28use waproto::whatsapp as wa;
29
30const MUTE_INDEFINITE: i64 = -1;
32
33pub(crate) fn dispatch_chat_mutation(
40 event_bus: &wacore::types::events::CoreEventBus,
41 m: &Mutation,
42 full_sync: bool,
43) -> bool {
44 if m.operation != wa::syncd_mutation::SyncdOperation::Set || m.index.is_empty() {
45 return false;
46 }
47
48 let kind = &m.index[0];
49
50 if !matches!(
53 kind.as_str(),
54 "mute"
55 | "pin"
56 | "pin_v1"
57 | "archive"
58 | "star"
59 | "contact"
60 | "mark_chat_as_read"
61 | "markChatAsRead"
62 ) {
63 return false;
64 }
65
66 let ts = m
67 .action_value
68 .as_ref()
69 .and_then(|v| v.timestamp)
70 .unwrap_or(0);
71 let time = DateTime::from_timestamp_millis(ts).unwrap_or_else(wacore::time::now_utc);
72 let jid: Jid = if m.index.len() > 1 {
73 match m.index[1].parse() {
74 Ok(j) => j,
75 Err(_) => {
76 log::warn!(
77 "Skipping chat mutation '{}': malformed JID '{}'",
78 kind,
79 m.index[1]
80 );
81 return true; }
83 }
84 } else {
85 log::warn!("Skipping chat mutation '{}': missing JID in index", kind);
86 return true;
87 };
88
89 match kind.as_str() {
90 "mute" => {
91 if let Some(val) = &m.action_value
92 && let Some(act) = &val.mute_action
93 {
94 event_bus.dispatch(&Event::MuteUpdate(MuteUpdate {
95 jid,
96 timestamp: time,
97 action: Box::new(*act),
98 from_full_sync: full_sync,
99 }));
100 }
101 true
102 }
103 "pin" | "pin_v1" => {
104 if let Some(val) = &m.action_value
105 && let Some(act) = &val.pin_action
106 {
107 event_bus.dispatch(&Event::PinUpdate(PinUpdate {
108 jid,
109 timestamp: time,
110 action: Box::new(*act),
111 from_full_sync: full_sync,
112 }));
113 }
114 true
115 }
116 "archive" => {
117 if let Some(val) = &m.action_value
118 && let Some(act) = &val.archive_chat_action
119 {
120 event_bus.dispatch(&Event::ArchiveUpdate(ArchiveUpdate {
121 jid,
122 timestamp: time,
123 action: Box::new(act.clone()),
124 from_full_sync: full_sync,
125 }));
126 }
127 true
128 }
129 "star" => {
130 if let Some(val) = &m.action_value
133 && let Some(act) = &val.star_action
134 && m.index.len() >= 5
135 {
136 let chat_jid: Jid = match m.index[1].parse() {
137 Ok(j) => j,
138 Err(_) => {
139 log::warn!(
140 "Skipping star mutation: malformed chat JID '{}'",
141 m.index[1]
142 );
143 return true;
144 }
145 };
146 let message_id = m.index[2].clone();
147 let from_me = m.index[3] == "1";
148 let participant_jid: Option<Jid> = if m.index[4] != "0" {
151 match m.index[4].parse() {
152 Ok(j) => Some(j),
153 Err(_) => {
154 log::warn!(
155 "Skipping star mutation: malformed participant JID '{}'",
156 m.index[4]
157 );
158 return true;
159 }
160 }
161 } else {
162 None
163 };
164
165 event_bus.dispatch(&Event::StarUpdate(StarUpdate {
166 chat_jid,
167 participant_jid,
168 message_id,
169 from_me,
170 timestamp: time,
171 action: Box::new(*act),
172 from_full_sync: full_sync,
173 }));
174 }
175 true
176 }
177 "contact" => {
178 if let Some(val) = &m.action_value
179 && let Some(act) = &val.contact_action
180 {
181 event_bus.dispatch(&Event::ContactUpdate(ContactUpdate {
182 jid,
183 timestamp: time,
184 action: Box::new(act.clone()),
185 from_full_sync: full_sync,
186 }));
187 }
188 true
189 }
190 "mark_chat_as_read" | "markChatAsRead" => {
191 if let Some(val) = &m.action_value
192 && let Some(act) = &val.mark_chat_as_read_action
193 {
194 event_bus.dispatch(&Event::MarkChatAsReadUpdate(MarkChatAsReadUpdate {
195 jid,
196 timestamp: time,
197 action: Box::new(act.clone()),
198 from_full_sync: full_sync,
199 }));
200 }
201 true
202 }
203 _ => false,
204 }
205}
206
207pub struct ChatActions<'a> {
213 client: &'a Arc<Client>,
214}
215
216impl<'a> ChatActions<'a> {
217 pub(crate) fn new(client: &'a Arc<Client>) -> Self {
218 Self { client }
219 }
220
221 pub async fn archive_chat(&self, jid: &Jid) -> Result<()> {
225 debug!("Archiving chat {jid}");
226 self.send_archive_mutation(jid, true).await
227 }
228
229 pub async fn unarchive_chat(&self, jid: &Jid) -> Result<()> {
231 debug!("Unarchiving chat {jid}");
232 self.send_archive_mutation(jid, false).await
233 }
234
235 pub async fn pin_chat(&self, jid: &Jid) -> Result<()> {
239 debug!("Pinning chat {jid}");
240 self.send_pin_mutation(jid, true).await
241 }
242
243 pub async fn unpin_chat(&self, jid: &Jid) -> Result<()> {
245 debug!("Unpinning chat {jid}");
246 self.send_pin_mutation(jid, false).await
247 }
248
249 pub async fn mute_chat(&self, jid: &Jid) -> Result<()> {
253 debug!("Muting chat {jid} indefinitely");
254 self.send_mute_mutation(jid, true, MUTE_INDEFINITE).await
255 }
256
257 pub async fn mute_chat_until(&self, jid: &Jid, mute_end_timestamp_ms: i64) -> Result<()> {
262 if mute_end_timestamp_ms <= 0 {
263 anyhow::bail!(
264 "mute_end_timestamp_ms must be a positive future timestamp (use mute_chat() for indefinite)"
265 );
266 }
267 let now_ms = wacore::time::now_millis();
268 if mute_end_timestamp_ms <= now_ms {
269 anyhow::bail!(
270 "mute_end_timestamp_ms is in the past ({mute_end_timestamp_ms} <= {now_ms})"
271 );
272 }
273 debug!("Muting chat {jid} until {mute_end_timestamp_ms}");
274 self.send_mute_mutation(jid, true, mute_end_timestamp_ms)
275 .await
276 }
277
278 pub async fn unmute_chat(&self, jid: &Jid) -> Result<()> {
280 debug!("Unmuting chat {jid}");
281 self.send_mute_mutation(jid, false, 0).await
282 }
283
284 pub async fn star_message(
294 &self,
295 chat_jid: &Jid,
296 participant_jid: Option<&Jid>,
297 message_id: &str,
298 from_me: bool,
299 ) -> Result<()> {
300 debug!("Starring message {message_id} in {chat_jid}");
301 self.send_star_mutation(chat_jid, participant_jid, message_id, from_me, true)
302 .await
303 }
304
305 pub async fn unstar_message(
309 &self,
310 chat_jid: &Jid,
311 participant_jid: Option<&Jid>,
312 message_id: &str,
313 from_me: bool,
314 ) -> Result<()> {
315 debug!("Unstarring message {message_id} in {chat_jid}");
316 self.send_star_mutation(chat_jid, participant_jid, message_id, from_me, false)
317 .await
318 }
319
320 async fn send_archive_mutation(&self, jid: &Jid, archived: bool) -> Result<()> {
323 let index = serde_json::to_vec(&["archive", &jid.to_string()])?;
324 let value = wa::SyncActionValue {
325 archive_chat_action: Some(wa::sync_action_value::ArchiveChatAction {
326 archived: Some(archived),
327 message_range: None,
328 }),
329 timestamp: Some(wacore::time::now_millis()),
330 ..Default::default()
331 };
332 self.send_mutation(WAPatchName::RegularLow, &index, &value)
333 .await
334 }
335
336 async fn send_pin_mutation(&self, jid: &Jid, pinned: bool) -> Result<()> {
337 let index = serde_json::to_vec(&["pin_v1", &jid.to_string()])?;
338 let value = wa::SyncActionValue {
339 pin_action: Some(wa::sync_action_value::PinAction {
340 pinned: Some(pinned),
341 }),
342 timestamp: Some(wacore::time::now_millis()),
343 ..Default::default()
344 };
345 self.send_mutation(WAPatchName::RegularLow, &index, &value)
346 .await
347 }
348
349 async fn send_mute_mutation(
350 &self,
351 jid: &Jid,
352 muted: bool,
353 mute_end_timestamp_ms: i64,
354 ) -> Result<()> {
355 let index = serde_json::to_vec(&["mute", &jid.to_string()])?;
356 let mute_end = if muted {
359 Some(mute_end_timestamp_ms)
360 } else {
361 Some(0)
362 };
363 let value = wa::SyncActionValue {
364 mute_action: Some(wa::sync_action_value::MuteAction {
365 muted: Some(muted),
366 mute_end_timestamp: mute_end,
367 auto_muted: None,
368 }),
369 timestamp: Some(wacore::time::now_millis()),
370 ..Default::default()
371 };
372 self.send_mutation(WAPatchName::RegularHigh, &index, &value)
373 .await
374 }
375
376 async fn send_star_mutation(
377 &self,
378 chat_jid: &Jid,
379 participant_jid: Option<&Jid>,
380 message_id: &str,
381 from_me: bool,
382 starred: bool,
383 ) -> Result<()> {
384 if chat_jid.is_group() && !from_me && participant_jid.is_none() {
385 anyhow::bail!(
386 "participant_jid is required when starring a group message not sent by us"
387 );
388 }
389 let from_me_str = if from_me { "1" } else { "0" };
393 let participant = participant_jid
394 .map(|j| j.to_string())
395 .unwrap_or_else(|| "0".to_string());
396 let index = serde_json::to_vec(&[
397 "star",
398 &chat_jid.to_string(),
399 message_id,
400 from_me_str,
401 &participant,
402 ])?;
403 let value = wa::SyncActionValue {
404 star_action: Some(wa::sync_action_value::StarAction {
405 starred: Some(starred),
406 }),
407 timestamp: Some(wacore::time::now_millis()),
408 ..Default::default()
409 };
410 self.send_mutation(WAPatchName::RegularHigh, &index, &value)
411 .await
412 }
413
414 async fn send_mutation(
416 &self,
417 collection: WAPatchName,
418 index: &[u8],
419 value: &wa::SyncActionValue,
420 ) -> Result<()> {
421 use rand::Rng;
422 use wacore::appstate::encode::encode_record;
423
424 let proc = self.client.get_app_state_processor().await;
425 let key_id = proc
426 .backend
427 .get_latest_sync_key_id()
428 .await
429 .map_err(|e| anyhow::anyhow!(e))?
430 .ok_or_else(|| anyhow::anyhow!("No app state sync key available"))?;
431 let keys = proc.get_app_state_key(&key_id).await?;
432
433 let mut iv = [0u8; 16];
434 rand::make_rng::<rand::rngs::StdRng>().fill_bytes(&mut iv);
435
436 let (mutation, value_mac) = encode_record(
437 wa::syncd_mutation::SyncdOperation::Set,
438 index,
439 value,
440 &keys,
441 &key_id,
442 &iv,
443 );
444
445 self.client
446 .send_app_state_patch(collection.as_str(), vec![(mutation, value_mac)])
447 .await
448 }
449}
450
451impl Client {
452 pub fn chat_actions(self: &Arc<Self>) -> ChatActions<'_> {
456 ChatActions::new(self)
457 }
458}