1use wacore::WireEnum;
8
9use crate::client::{Client, ClientError};
10use crate::features::mex::{MexError, mex_request};
11use crate::request::IqError;
12use thiserror::Error;
13use wacore::iq::mex_operations::{
14 create_newsletter, fetch_all_newsletters_metadata, fetch_newsletter, join_newsletter,
15 leave_newsletter, update_newsletter, update_newsletter_user_setting,
16};
17use wacore::iq::newsletter::NEWSLETTER_XMLNS;
18use wacore::request::InfoQuery;
19use wacore_binary::Jid;
20use wacore_binary::JidExt as _;
21use wacore_binary::builder::NodeBuilder;
22use wacore_binary::{NodeContent, NodeContentRef, NodeRef};
23use waproto::whatsapp as wa;
24
25#[derive(Debug, Error)]
27#[non_exhaustive]
28pub enum NewsletterError {
29 #[error("{0}")]
31 Mex(#[from] MexError),
32 #[error("{0}")]
34 Iq(#[from] IqError),
35 #[error("{0}")]
37 Client(#[from] ClientError),
38 #[error("invalid newsletter request: {0}")]
41 InvalidRequest(String),
42 #[error("{0}")]
44 Internal(#[from] anyhow::Error),
45}
46
47impl NewsletterError {
48 pub(crate) fn from_anyhow(err: anyhow::Error) -> Self {
53 match err.downcast::<ClientError>() {
54 Ok(ClientError::Iq(iq)) => NewsletterError::Iq(iq),
55 Ok(client) => NewsletterError::Client(client),
56 Err(other) => match other.downcast::<IqError>() {
57 Ok(iq) => NewsletterError::Iq(iq),
58 Err(other) => NewsletterError::Internal(other),
59 },
60 }
61 }
62}
63
64#[derive(Debug, Clone, PartialEq, Eq, WireEnum)]
67#[non_exhaustive]
68pub enum NewsletterMessageType {
69 #[wire = "text"]
70 Text,
71 #[wire = "media"]
72 Media,
73 #[wire = "reaction"]
74 Reaction,
75 #[wire = "revoke"]
76 Revoke,
77 #[wire = "poll_creation"]
78 PollCreation,
79 #[wire = "poll_vote"]
80 PollVote,
81 #[wire = "edit"]
82 Edit,
83 #[wire_fallback]
84 Other(String),
85}
86
87#[derive(Debug, Clone, PartialEq, Eq)]
89#[non_exhaustive]
90pub enum NewsletterVerification {
91 Verified,
92 Unverified,
93}
94
95#[derive(Debug, Clone, PartialEq, Eq)]
97#[non_exhaustive]
98pub enum NewsletterState {
99 Active,
100 Suspended,
101 Geosuspended,
102}
103
104#[derive(Debug, Clone, PartialEq, Eq)]
106#[non_exhaustive]
107pub enum NewsletterRole {
108 Owner,
109 Admin,
110 Subscriber,
111 Guest,
112}
113
114#[derive(Debug, Clone, PartialEq, Eq)]
116pub struct NewsletterMetadata {
117 pub jid: Jid,
118 pub name: String,
119 pub description: Option<String>,
120 pub subscriber_count: u64,
121 pub verification: NewsletterVerification,
122 pub state: NewsletterState,
123 pub picture_url: Option<String>,
124 pub preview_url: Option<String>,
125 pub invite_code: Option<String>,
126 pub role: Option<NewsletterRole>,
127 pub creation_time: Option<u64>,
128}
129
130#[derive(Debug, Clone)]
132pub struct NewsletterReactionCount {
133 pub code: String,
134 pub count: u64,
135}
136
137#[derive(Debug, Clone)]
139pub struct NewsletterMessage {
140 pub message_id: String,
143 pub server_id: u64,
145 pub timestamp: u64,
147 pub message_type: NewsletterMessageType,
149 pub is_sender: bool,
151 pub message: Option<wa::Message>,
153 pub reactions: Vec<NewsletterReactionCount>,
155}
156
157pub struct Newsletter<'a> {
159 client: &'a Client,
160}
161
162impl<'a> Newsletter<'a> {
163 pub(crate) fn new(client: &'a Client) -> Self {
164 Self { client }
165 }
166
167 pub async fn list_subscribed(&self) -> Result<Vec<NewsletterMetadata>, NewsletterError> {
169 let response = self
170 .client
171 .mex()
172 .query(mex_request!(fetch_all_newsletters_metadata {
173 ..Default::default()
174 }))
175 .await?;
176
177 let data = response
178 .data
179 .ok_or_else(|| NewsletterError::InvalidRequest("missing data".into()))?;
180 let newsletters = data["xwa2_newsletter_subscribed"]
181 .as_array()
182 .ok_or_else(|| {
183 NewsletterError::InvalidRequest("missing xwa2_newsletter_subscribed array".into())
184 })?;
185
186 newsletters.iter().map(parse_newsletter_metadata).collect()
187 }
188
189 pub async fn get_metadata(&self, jid: &Jid) -> Result<NewsletterMetadata, NewsletterError> {
191 let response = self
192 .client
193 .mex()
194 .query(mex_request!(fetch_newsletter {
195 input: Some(fetch_newsletter::Input {
196 key: Some(jid.to_string()),
197 r#type: Some("JID".into()),
198 view_role: Some("GUEST".into()),
199 }),
200 fetch_viewer_metadata: Some(true),
201 fetch_full_image: Some(true),
202 fetch_creation_time: Some(true),
203 ..Default::default()
204 }))
205 .await?;
206
207 let data = response
208 .data
209 .ok_or_else(|| NewsletterError::InvalidRequest("missing data".into()))?;
210 let newsletter = &data["xwa2_newsletter"];
211 if newsletter.is_null() {
212 return Err(NewsletterError::InvalidRequest(format!(
213 "newsletter not found: {}",
214 jid
215 )));
216 }
217 parse_newsletter_metadata(newsletter)
218 }
219
220 pub async fn create(
224 &self,
225 name: &str,
226 description: Option<&str>,
227 ) -> Result<NewsletterMetadata, NewsletterError> {
228 let response = self
229 .client
230 .mex()
231 .mutate(mex_request!(create_newsletter {
232 input: Some(create_newsletter::Input {
233 name: Some(name.to_string()),
234 description: description.map(str::to_string),
235 picture: None,
236 }),
237 }))
238 .await?;
239
240 let data = response
241 .data
242 .ok_or_else(|| NewsletterError::InvalidRequest("missing data".into()))?;
243 let newsletter = &data["xwa2_newsletter_create"];
244 if newsletter.is_null() {
245 return Err(NewsletterError::InvalidRequest(
246 "newsletter creation failed".into(),
247 ));
248 }
249 parse_newsletter_metadata(newsletter)
250 }
251
252 pub async fn join(&self, jid: &Jid) -> Result<NewsletterMetadata, NewsletterError> {
256 let response = self
257 .client
258 .mex()
259 .mutate(mex_request!(join_newsletter {
260 newsletter_id: Some(jid.to_string()),
261 }))
262 .await?;
263
264 let data = response
265 .data
266 .ok_or_else(|| NewsletterError::InvalidRequest("missing data".into()))?;
267 let newsletter = &data["xwa2_newsletter_join_v2"];
268 if newsletter.is_null() {
269 return Err(NewsletterError::InvalidRequest(format!(
270 "failed to join newsletter: {}",
271 jid
272 )));
273 }
274 parse_newsletter_metadata(newsletter)
275 }
276
277 pub async fn leave(&self, jid: &Jid) -> Result<(), NewsletterError> {
279 let response = self
280 .client
281 .mex()
282 .mutate(mex_request!(leave_newsletter {
283 newsletter_id: Some(jid.to_string()),
284 }))
285 .await?;
286
287 let data = response
288 .data
289 .ok_or_else(|| NewsletterError::InvalidRequest("missing data".into()))?;
290 if data["xwa2_newsletter_leave_v2"].is_null() {
291 return Err(NewsletterError::InvalidRequest(format!(
292 "failed to leave newsletter: {}",
293 jid
294 )));
295 }
296 Ok(())
297 }
298
299 pub async fn update(
301 &self,
302 jid: &Jid,
303 name: Option<&str>,
304 description: Option<&str>,
305 ) -> Result<NewsletterMetadata, NewsletterError> {
306 let response = self
307 .client
308 .mex()
309 .mutate(mex_request!(update_newsletter {
310 newsletter_id: Some(jid.to_string()),
311 updates: Some(update_newsletter::Updates {
312 name: name.map(str::to_string),
313 description: description.map(str::to_string),
314 picture: None,
315 settings: None,
316 }),
317 }))
318 .await?;
319
320 let data = response
321 .data
322 .ok_or_else(|| NewsletterError::InvalidRequest("missing data".into()))?;
323 let newsletter = &data["xwa2_newsletter_update"];
324 if newsletter.is_null() {
325 return Err(NewsletterError::InvalidRequest(format!(
326 "failed to update newsletter: {}",
327 jid
328 )));
329 }
330 parse_newsletter_metadata(newsletter)
331 }
332
333 pub async fn set_follower_mute(&self, jid: &Jid, muted: bool) -> Result<(), NewsletterError> {
336 self.set_user_setting_mute(jid, "MUTE_FOLLOWER_ACTIVITY", muted)
337 .await
338 }
339
340 pub async fn set_admin_mute(&self, jid: &Jid, muted: bool) -> Result<(), NewsletterError> {
343 self.set_user_setting_mute(jid, "MUTE_ADMIN_ACTIVITY", muted)
344 .await
345 }
346
347 async fn set_user_setting_mute(
348 &self,
349 jid: &Jid,
350 mute_type: &str,
351 muted: bool,
352 ) -> Result<(), NewsletterError> {
353 let response = self
354 .client
355 .mex()
356 .mutate(mex_request!(
357 update_newsletter_user_setting,
358 mute_user_setting_variables(jid, mute_type, muted)
359 ))
360 .await?;
361
362 let data = response
363 .data
364 .ok_or_else(|| NewsletterError::InvalidRequest("missing data".into()))?;
365 if data["xwa2_newsletter_update_user_setting"].is_null() {
366 return Err(NewsletterError::InvalidRequest(format!(
367 "failed to update newsletter user setting: {jid}"
368 )));
369 }
370 Ok(())
371 }
372
373 pub async fn get_metadata_by_invite(
375 &self,
376 invite_code: &str,
377 ) -> Result<NewsletterMetadata, NewsletterError> {
378 let response = self
379 .client
380 .mex()
381 .query(mex_request!(fetch_newsletter {
382 input: Some(fetch_newsletter::Input {
383 key: Some(invite_code.to_string()),
384 r#type: Some("INVITE".into()),
385 view_role: Some("GUEST".into()),
386 }),
387 fetch_viewer_metadata: Some(true),
388 fetch_full_image: Some(true),
389 fetch_creation_time: Some(true),
390 ..Default::default()
391 }))
392 .await?;
393
394 let data = response
395 .data
396 .ok_or_else(|| NewsletterError::InvalidRequest("missing data".into()))?;
397 let newsletter = &data["xwa2_newsletter"];
398 if newsletter.is_null() {
399 return Err(NewsletterError::InvalidRequest(format!(
400 "newsletter not found for invite: {}",
401 invite_code
402 )));
403 }
404 parse_newsletter_metadata(newsletter)
405 }
406
407 pub async fn subscribe_live_updates(
415 &self,
416 jid: impl Into<Jid>,
417 ) -> Result<u64, NewsletterError> {
418 let jid = &jid.into();
419 let iq = InfoQuery::set(
420 NEWSLETTER_XMLNS,
421 jid.clone(),
422 Some(NodeContent::Nodes(vec![
423 NodeBuilder::new("live_updates").build(),
424 ])),
425 );
426
427 let response = self.client.send_iq(iq).await?;
428 let nr = response.get();
429 let duration = nr
430 .get_optional_child("live_updates")
431 .and_then(|n| n.get_attr("duration"))
432 .map(|v| v.as_str())
433 .and_then(|s| s.parse::<u64>().ok())
434 .unwrap_or(300);
435
436 Ok(duration)
437 }
438
439 pub async fn send_reaction(
444 &self,
445 jid: &Jid,
446 server_id: u64,
447 reaction: &str,
448 ) -> Result<(), NewsletterError> {
449 self.client
450 .send_server_reaction(jid, server_id, reaction)
451 .await
452 .map_err(NewsletterError::from_anyhow)?;
453 Ok(())
454 }
455
456 pub async fn edit_message(
464 &self,
465 jid: &Jid,
466 message_id: impl Into<String>,
467 new_content: wa::Message,
468 ) -> Result<(), NewsletterError> {
469 if !jid.is_newsletter() {
470 return Err(NewsletterError::InvalidRequest(
471 "edit_message is only valid for newsletter (channel) JIDs; use Client::edit_message for DM/group".into(),
472 ));
473 }
474 let id = message_id.into();
475 if id.is_empty() {
476 return Err(NewsletterError::InvalidRequest(
477 "newsletter edit needs a target message_id (NewsletterMessage.message_id is empty when the server omits the id)".into(),
478 ));
479 }
480 let node = crate::send::build_newsletter_edit_node(
481 jid,
482 &id,
483 crate::send::NewsletterEdit::Edit(&new_content),
484 );
485 self.client.send_node(node).await?;
486 Ok(())
487 }
488
489 pub async fn revoke_message(
494 &self,
495 jid: &Jid,
496 message_id: impl Into<String>,
497 ) -> Result<(), NewsletterError> {
498 if !jid.is_newsletter() {
499 return Err(NewsletterError::InvalidRequest(
500 "revoke_message is only valid for newsletter (channel) JIDs; use Client::revoke_message for DM/group".into(),
501 ));
502 }
503 let id = message_id.into();
504 if id.is_empty() {
505 return Err(NewsletterError::InvalidRequest(
506 "newsletter revoke needs a target message_id (NewsletterMessage.message_id is empty when the server omits the id)".into(),
507 ));
508 }
509 let node =
510 crate::send::build_newsletter_edit_node(jid, &id, crate::send::NewsletterEdit::Revoke);
511 self.client.send_node(node).await?;
512 Ok(())
513 }
514
515 pub async fn get_messages(
520 &self,
521 jid: impl Into<Jid>,
522 count: u32,
523 before: Option<u64>,
524 ) -> Result<Vec<NewsletterMessage>, NewsletterError> {
525 let jid = &jid.into();
526 let mut messages_node = NodeBuilder::new("messages").attr("count", count);
527 if let Some(before_id) = before {
528 messages_node = messages_node.attr("before", before_id);
529 }
530
531 let iq = InfoQuery::get(
532 NEWSLETTER_XMLNS,
533 jid.clone(),
534 Some(NodeContent::Nodes(vec![messages_node.build()])),
535 );
536
537 let response = self.client.send_iq(iq).await?;
538 parse_newsletter_messages_response(response.get())
539 }
540}
541
542impl Client {
543 #[inline]
545 pub fn newsletter(&self) -> Newsletter<'_> {
546 Newsletter::new(self)
547 }
548}
549
550fn parse_newsletter_metadata(
553 value: &serde_json::Value,
554) -> Result<NewsletterMetadata, NewsletterError> {
555 let jid_str = value["id"]
556 .as_str()
557 .ok_or_else(|| NewsletterError::InvalidRequest("missing newsletter id".into()))?;
558 let jid: Jid = jid_str
559 .parse()
560 .map_err(|e| NewsletterError::InvalidRequest(format!("invalid newsletter id: {e}")))?;
561
562 let thread = &value["thread_metadata"];
563
564 let name = thread["name"]["text"].as_str().unwrap_or("").to_string();
565 let description = thread["description"]["text"]
566 .as_str()
567 .filter(|s| !s.is_empty())
568 .map(|s| s.to_string());
569
570 let subscriber_count = thread["subscribers_count"]
571 .as_str()
572 .and_then(|s| s.parse::<u64>().ok())
573 .unwrap_or(0);
574
575 let verification = match thread["verification"].as_str() {
576 Some("VERIFIED") => NewsletterVerification::Verified,
577 _ => NewsletterVerification::Unverified,
578 };
579
580 let state = match value["state"]["type"].as_str() {
581 Some("suspended") => NewsletterState::Suspended,
582 Some("geosuspended") => NewsletterState::Geosuspended,
583 _ => NewsletterState::Active,
584 };
585
586 let picture_url = thread["picture"]["direct_path"]
587 .as_str()
588 .map(|s| s.to_string());
589 let preview_url = thread["preview"]["direct_path"]
590 .as_str()
591 .map(|s| s.to_string());
592 let invite_code = thread["invite"].as_str().map(|s| s.to_string());
593
594 let creation_time = thread["creation_time"]
595 .as_str()
596 .and_then(|s| s.parse::<u64>().ok());
597
598 let role = value["viewer_metadata"]["role"]
599 .as_str()
600 .and_then(|r| match r {
601 "owner" => Some(NewsletterRole::Owner),
602 "admin" => Some(NewsletterRole::Admin),
603 "subscriber" => Some(NewsletterRole::Subscriber),
604 "guest" => Some(NewsletterRole::Guest),
605 _ => None,
606 });
607
608 Ok(NewsletterMetadata {
609 jid,
610 name,
611 description,
612 subscriber_count,
613 verification,
614 state,
615 picture_url,
616 preview_url,
617 invite_code,
618 role,
619 creation_time,
620 })
621}
622
623pub(crate) fn parse_reaction_counts(node: &NodeRef<'_>) -> Vec<NewsletterReactionCount> {
628 let mut reactions = Vec::new();
629 if let Some(reactions_node) = node.get_optional_child("reactions")
630 && let Some(children) = reactions_node.children()
631 {
632 for r in children.iter().filter(|n| n.tag.as_ref() == "reaction") {
633 let Some(code) = r
634 .get_attr("code")
635 .map(|v| v.as_str())
636 .filter(|s| !s.is_empty())
637 .map(|s| s.into_owned())
638 else {
639 continue;
640 };
641 let count = r
642 .get_attr("count")
643 .map(|v| v.as_str())
644 .and_then(|s| s.parse::<u64>().ok())
645 .unwrap_or(0);
646 reactions.push(NewsletterReactionCount { code, count });
647 }
648 }
649 reactions
650}
651
652fn parse_newsletter_messages_response(
666 response: &NodeRef<'_>,
667) -> Result<Vec<NewsletterMessage>, NewsletterError> {
668 let messages_node = response.get_optional_child("messages").ok_or_else(|| {
670 NewsletterError::InvalidRequest("missing <messages> in newsletter response".into())
671 })?;
672
673 let children = match messages_node.children() {
674 Some(c) => c,
675 None => return Ok(vec![]),
676 };
677
678 let mut result = Vec::with_capacity(children.len());
679 for msg_node in children.iter().filter(|n| n.tag.as_ref() == "message") {
680 let Some(server_id) = msg_node
682 .get_attr("server_id")
683 .map(|v| v.as_str())
684 .and_then(|s| s.parse::<u64>().ok())
685 else {
686 continue;
687 };
688
689 let message_id = msg_node
692 .get_attr("id")
693 .map(|v| v.as_str().into_owned())
694 .unwrap_or_default();
695
696 let timestamp = msg_node
697 .get_attr("t")
698 .map(|v| v.as_str())
699 .and_then(|s| s.parse::<u64>().ok())
700 .unwrap_or(0);
701
702 let message_type = msg_node
703 .get_attr("type")
704 .map(|v| v.as_str())
705 .map(|s| NewsletterMessageType::from(s.as_ref()))
706 .unwrap_or(NewsletterMessageType::Text);
707
708 let is_sender = msg_node
709 .get_attr("is_sender")
710 .is_some_and(|v| v.as_str() == "true");
711
712 let message =
714 msg_node
715 .get_optional_child("plaintext")
716 .and_then(|pt| match pt.content.as_ref() {
717 Some(NodeContentRef::Bytes(bytes)) => {
718 waproto::codec::message_decode(bytes.as_ref()).ok()
719 }
720 _ => None,
721 });
722
723 let reactions = parse_reaction_counts(msg_node);
724
725 result.push(NewsletterMessage {
726 message_id,
727 server_id,
728 timestamp,
729 message_type,
730 is_sender,
731 message,
732 reactions,
733 });
734 }
735
736 Ok(result)
737}
738
739fn mute_user_setting_variables(jid: &Jid, mute_type: &str, muted: bool) -> serde_json::Value {
745 serde_json::json!({
746 "input": {
747 "newsletter_id": jid.to_string(),
748 "type": mute_type,
749 "value": if muted { "ON" } else { "OFF" },
750 }
751 })
752}
753
754#[cfg(test)]
755mod tests {
756 use super::*;
757 use wacore_binary::builder::NodeBuilder;
758
759 #[test]
760 fn mute_variables_match_wa_web_shape() {
761 let jid: Jid = "111222333@newsletter".parse().unwrap();
762 let on = mute_user_setting_variables(&jid, "MUTE_FOLLOWER_ACTIVITY", true);
763 assert_eq!(on["input"]["newsletter_id"], "111222333@newsletter");
764 assert_eq!(on["input"]["type"], "MUTE_FOLLOWER_ACTIVITY");
765 assert_eq!(on["input"]["value"], "ON");
766
767 let off = mute_user_setting_variables(&jid, "MUTE_ADMIN_ACTIVITY", false);
768 assert_eq!(off["input"]["type"], "MUTE_ADMIN_ACTIVITY");
769 assert_eq!(off["input"]["value"], "OFF");
770 }
771
772 #[test]
773 fn test_missing_type_attribute_defaults_to_text() {
774 let response = NodeBuilder::new("iq")
775 .children([NodeBuilder::new("messages")
776 .children([NodeBuilder::new("message")
777 .attr("server_id", "42")
778 .attr("t", "1700000000")
779 .build()])
780 .build()])
781 .build();
782
783 let msgs = parse_newsletter_messages_response(&response.as_node_ref()).unwrap();
784 assert_eq!(msgs.len(), 1);
785 assert_eq!(msgs[0].message_type, NewsletterMessageType::Text);
786 }
787
788 #[test]
789 fn test_explicit_type_attribute_parsed() {
790 let response = NodeBuilder::new("iq")
791 .children([NodeBuilder::new("messages")
792 .children([NodeBuilder::new("message")
793 .attr("server_id", "1")
794 .attr("t", "1700000000")
795 .attr("type", "media")
796 .build()])
797 .build()])
798 .build();
799
800 let msgs = parse_newsletter_messages_response(&response.as_node_ref()).unwrap();
801 assert_eq!(msgs[0].message_type, NewsletterMessageType::Media);
802 }
803}