1use wacore::WireEnum;
8
9use crate::client::Client;
10use crate::features::mex::{MexError, MexRequest};
11use prost::Message as ProtoMessage;
12use serde_json::json;
13use wacore::iq::mex_ids::newsletter as newsletter_docs;
14use wacore::iq::newsletter::NEWSLETTER_XMLNS;
15use wacore::request::InfoQuery;
16use wacore_binary::Jid;
17use wacore_binary::builder::NodeBuilder;
18use wacore_binary::{NodeContent, NodeContentRef, NodeRef};
19use waproto::whatsapp as wa;
20
21#[derive(Debug, Clone, PartialEq, Eq, WireEnum)]
24#[non_exhaustive]
25pub enum NewsletterMessageType {
26 #[wire = "text"]
27 Text,
28 #[wire = "media"]
29 Media,
30 #[wire = "reaction"]
31 Reaction,
32 #[wire = "revoke"]
33 Revoke,
34 #[wire = "poll_creation"]
35 PollCreation,
36 #[wire = "poll_vote"]
37 PollVote,
38 #[wire = "edit"]
39 Edit,
40 #[wire_fallback]
41 Other(String),
42}
43
44#[derive(Debug, Clone, PartialEq, Eq)]
46#[non_exhaustive]
47pub enum NewsletterVerification {
48 Verified,
49 Unverified,
50}
51
52#[derive(Debug, Clone, PartialEq, Eq)]
54#[non_exhaustive]
55pub enum NewsletterState {
56 Active,
57 Suspended,
58 Geosuspended,
59}
60
61#[derive(Debug, Clone, PartialEq, Eq)]
63#[non_exhaustive]
64pub enum NewsletterRole {
65 Owner,
66 Admin,
67 Subscriber,
68 Guest,
69}
70
71#[derive(Debug, Clone, PartialEq, Eq)]
73pub struct NewsletterMetadata {
74 pub jid: Jid,
75 pub name: String,
76 pub description: Option<String>,
77 pub subscriber_count: u64,
78 pub verification: NewsletterVerification,
79 pub state: NewsletterState,
80 pub picture_url: Option<String>,
81 pub preview_url: Option<String>,
82 pub invite_code: Option<String>,
83 pub role: Option<NewsletterRole>,
84 pub creation_time: Option<u64>,
85}
86
87#[derive(Debug, Clone)]
89pub struct NewsletterReactionCount {
90 pub code: String,
91 pub count: u64,
92}
93
94#[derive(Debug, Clone)]
96pub struct NewsletterMessage {
97 pub server_id: u64,
99 pub timestamp: u64,
101 pub message_type: NewsletterMessageType,
103 pub is_sender: bool,
105 pub message: Option<wa::Message>,
107 pub reactions: Vec<NewsletterReactionCount>,
109}
110
111pub struct Newsletter<'a> {
113 client: &'a Client,
114}
115
116impl<'a> Newsletter<'a> {
117 pub(crate) fn new(client: &'a Client) -> Self {
118 Self { client }
119 }
120
121 pub async fn list_subscribed(&self) -> Result<Vec<NewsletterMetadata>, MexError> {
123 let response = self
124 .client
125 .mex()
126 .query(MexRequest {
127 doc: newsletter_docs::LIST_SUBSCRIBED,
128 variables: json!({}),
129 })
130 .await?;
131
132 let data = response
133 .data
134 .ok_or_else(|| MexError::PayloadParsing("missing data".into()))?;
135 let newsletters = data["xwa2_newsletter_subscribed"]
136 .as_array()
137 .ok_or_else(|| {
138 MexError::PayloadParsing("missing xwa2_newsletter_subscribed array".into())
139 })?;
140
141 newsletters.iter().map(parse_newsletter_metadata).collect()
142 }
143
144 pub async fn get_metadata(&self, jid: &Jid) -> Result<NewsletterMetadata, MexError> {
146 let response = self
147 .client
148 .mex()
149 .query(MexRequest {
150 doc: newsletter_docs::FETCH_METADATA,
151 variables: json!({
152 "input": {
153 "key": jid.to_string(),
154 "type": "JID",
155 "view_role": "GUEST"
156 },
157 "fetch_viewer_metadata": true,
158 "fetch_full_image": true,
159 "fetch_creation_time": true
160 }),
161 })
162 .await?;
163
164 let data = response
165 .data
166 .ok_or_else(|| MexError::PayloadParsing("missing data".into()))?;
167 let newsletter = &data["xwa2_newsletter"];
168 if newsletter.is_null() {
169 return Err(MexError::PayloadParsing(format!(
170 "newsletter not found: {}",
171 jid
172 )));
173 }
174 parse_newsletter_metadata(newsletter)
175 }
176
177 pub async fn create(
181 &self,
182 name: &str,
183 description: Option<&str>,
184 ) -> Result<NewsletterMetadata, MexError> {
185 let mut input = json!({ "name": name });
186 if let Some(desc) = description {
187 input["description"] = json!(desc);
188 }
189
190 let response = self
191 .client
192 .mex()
193 .mutate(MexRequest {
194 doc: newsletter_docs::CREATE,
195 variables: json!({ "input": input }),
196 })
197 .await?;
198
199 let data = response
200 .data
201 .ok_or_else(|| MexError::PayloadParsing("missing data".into()))?;
202 let newsletter = &data["xwa2_newsletter_create"];
203 if newsletter.is_null() {
204 return Err(MexError::PayloadParsing(
205 "newsletter creation failed".into(),
206 ));
207 }
208 parse_newsletter_metadata(newsletter)
209 }
210
211 pub async fn join(&self, jid: &Jid) -> Result<NewsletterMetadata, MexError> {
215 let response = self
216 .client
217 .mex()
218 .mutate(MexRequest {
219 doc: newsletter_docs::JOIN,
220 variables: json!({
221 "newsletter_id": jid.to_string()
222 }),
223 })
224 .await?;
225
226 let data = response
227 .data
228 .ok_or_else(|| MexError::PayloadParsing("missing data".into()))?;
229 let newsletter = &data["xwa2_newsletter_join_v2"];
230 if newsletter.is_null() {
231 return Err(MexError::PayloadParsing(format!(
232 "failed to join newsletter: {}",
233 jid
234 )));
235 }
236 parse_newsletter_metadata(newsletter)
237 }
238
239 pub async fn leave(&self, jid: &Jid) -> Result<(), MexError> {
241 let response = self
242 .client
243 .mex()
244 .mutate(MexRequest {
245 doc: newsletter_docs::LEAVE,
246 variables: json!({
247 "newsletter_id": jid.to_string()
248 }),
249 })
250 .await?;
251
252 let data = response
253 .data
254 .ok_or_else(|| MexError::PayloadParsing("missing data".into()))?;
255 if data["xwa2_newsletter_leave_v2"].is_null() {
256 return Err(MexError::PayloadParsing(format!(
257 "failed to leave newsletter: {}",
258 jid
259 )));
260 }
261 Ok(())
262 }
263
264 pub async fn update(
266 &self,
267 jid: &Jid,
268 name: Option<&str>,
269 description: Option<&str>,
270 ) -> Result<NewsletterMetadata, MexError> {
271 let mut updates = json!({});
272 if let Some(name) = name {
273 updates["name"] = json!(name);
274 }
275 if let Some(desc) = description {
276 updates["description"] = json!(desc);
277 }
278
279 let response = self
280 .client
281 .mex()
282 .mutate(MexRequest {
283 doc: newsletter_docs::UPDATE,
284 variables: json!({
285 "newsletter_id": jid.to_string(),
286 "updates": updates
287 }),
288 })
289 .await?;
290
291 let data = response
292 .data
293 .ok_or_else(|| MexError::PayloadParsing("missing data".into()))?;
294 let newsletter = &data["xwa2_newsletter_update"];
295 if newsletter.is_null() {
296 return Err(MexError::PayloadParsing(format!(
297 "failed to update newsletter: {}",
298 jid
299 )));
300 }
301 parse_newsletter_metadata(newsletter)
302 }
303
304 pub async fn get_metadata_by_invite(
306 &self,
307 invite_code: &str,
308 ) -> Result<NewsletterMetadata, MexError> {
309 let response = self
310 .client
311 .mex()
312 .query(MexRequest {
313 doc: newsletter_docs::FETCH_METADATA,
314 variables: json!({
315 "input": {
316 "key": invite_code,
317 "type": "INVITE",
318 "view_role": "GUEST"
319 },
320 "fetch_viewer_metadata": true,
321 "fetch_full_image": true,
322 "fetch_creation_time": true
323 }),
324 })
325 .await?;
326
327 let data = response
328 .data
329 .ok_or_else(|| MexError::PayloadParsing("missing data".into()))?;
330 let newsletter = &data["xwa2_newsletter"];
331 if newsletter.is_null() {
332 return Err(MexError::PayloadParsing(format!(
333 "newsletter not found for invite: {}",
334 invite_code
335 )));
336 }
337 parse_newsletter_metadata(newsletter)
338 }
339
340 pub async fn subscribe_live_updates(&self, jid: &Jid) -> Result<u64, anyhow::Error> {
348 let iq = InfoQuery::set(
349 NEWSLETTER_XMLNS,
350 jid.clone(),
351 Some(NodeContent::Nodes(vec![
352 NodeBuilder::new("live_updates").build(),
353 ])),
354 );
355
356 let response = self.client.send_iq(iq).await?;
357 let nr = response.get();
358 let duration = nr
359 .get_optional_child("live_updates")
360 .and_then(|n| n.get_attr("duration"))
361 .map(|v| v.as_str())
362 .and_then(|s| s.parse::<u64>().ok())
363 .unwrap_or(300);
364
365 Ok(duration)
366 }
367
368 pub async fn send_reaction(
373 &self,
374 jid: &Jid,
375 server_id: u64,
376 reaction: &str,
377 ) -> Result<(), anyhow::Error> {
378 self.client
379 .send_server_reaction(jid, server_id, reaction)
380 .await
381 }
382
383 pub async fn get_messages(
388 &self,
389 jid: &Jid,
390 count: u32,
391 before: Option<u64>,
392 ) -> Result<Vec<NewsletterMessage>, anyhow::Error> {
393 let mut messages_node = NodeBuilder::new("messages").attr("count", count);
394 if let Some(before_id) = before {
395 messages_node = messages_node.attr("before", before_id);
396 }
397
398 let iq = InfoQuery::get(
399 NEWSLETTER_XMLNS,
400 jid.clone(),
401 Some(NodeContent::Nodes(vec![messages_node.build()])),
402 );
403
404 let response = self.client.send_iq(iq).await?;
405 parse_newsletter_messages_response(response.get())
406 }
407}
408
409impl Client {
410 #[inline]
412 pub fn newsletter(&self) -> Newsletter<'_> {
413 Newsletter::new(self)
414 }
415}
416
417fn parse_newsletter_metadata(value: &serde_json::Value) -> Result<NewsletterMetadata, MexError> {
420 let jid_str = value["id"]
421 .as_str()
422 .ok_or_else(|| MexError::PayloadParsing("missing newsletter id".into()))?;
423 let jid: Jid = jid_str.parse()?;
424
425 let thread = &value["thread_metadata"];
426
427 let name = thread["name"]["text"].as_str().unwrap_or("").to_string();
428 let description = thread["description"]["text"]
429 .as_str()
430 .filter(|s| !s.is_empty())
431 .map(|s| s.to_string());
432
433 let subscriber_count = thread["subscribers_count"]
434 .as_str()
435 .and_then(|s| s.parse::<u64>().ok())
436 .unwrap_or(0);
437
438 let verification = match thread["verification"].as_str() {
439 Some("VERIFIED") => NewsletterVerification::Verified,
440 _ => NewsletterVerification::Unverified,
441 };
442
443 let state = match value["state"]["type"].as_str() {
444 Some("suspended") => NewsletterState::Suspended,
445 Some("geosuspended") => NewsletterState::Geosuspended,
446 _ => NewsletterState::Active,
447 };
448
449 let picture_url = thread["picture"]["direct_path"]
450 .as_str()
451 .map(|s| s.to_string());
452 let preview_url = thread["preview"]["direct_path"]
453 .as_str()
454 .map(|s| s.to_string());
455 let invite_code = thread["invite"].as_str().map(|s| s.to_string());
456
457 let creation_time = thread["creation_time"]
458 .as_str()
459 .and_then(|s| s.parse::<u64>().ok());
460
461 let role = value["viewer_metadata"]["role"]
462 .as_str()
463 .and_then(|r| match r {
464 "owner" => Some(NewsletterRole::Owner),
465 "admin" => Some(NewsletterRole::Admin),
466 "subscriber" => Some(NewsletterRole::Subscriber),
467 "guest" => Some(NewsletterRole::Guest),
468 _ => None,
469 });
470
471 Ok(NewsletterMetadata {
472 jid,
473 name,
474 description,
475 subscriber_count,
476 verification,
477 state,
478 picture_url,
479 preview_url,
480 invite_code,
481 role,
482 creation_time,
483 })
484}
485
486pub(crate) fn parse_reaction_counts(node: &NodeRef<'_>) -> Vec<NewsletterReactionCount> {
491 let mut reactions = Vec::new();
492 if let Some(reactions_node) = node.get_optional_child("reactions")
493 && let Some(children) = reactions_node.children()
494 {
495 for r in children.iter().filter(|n| n.tag.as_ref() == "reaction") {
496 let Some(code) = r
497 .get_attr("code")
498 .map(|v| v.as_str())
499 .filter(|s| !s.is_empty())
500 .map(|s| s.into_owned())
501 else {
502 continue;
503 };
504 let count = r
505 .get_attr("count")
506 .map(|v| v.as_str())
507 .and_then(|s| s.parse::<u64>().ok())
508 .unwrap_or(0);
509 reactions.push(NewsletterReactionCount { code, count });
510 }
511 }
512 reactions
513}
514
515fn parse_newsletter_messages_response(
529 response: &NodeRef<'_>,
530) -> Result<Vec<NewsletterMessage>, anyhow::Error> {
531 let messages_node = response
533 .get_optional_child("messages")
534 .ok_or_else(|| anyhow::anyhow!("missing <messages> in newsletter response"))?;
535
536 let children = match messages_node.children() {
537 Some(c) => c,
538 None => return Ok(vec![]),
539 };
540
541 let mut result = Vec::with_capacity(children.len());
542 for msg_node in children.iter().filter(|n| n.tag.as_ref() == "message") {
543 let Some(server_id) = msg_node
545 .get_attr("server_id")
546 .map(|v| v.as_str())
547 .and_then(|s| s.parse::<u64>().ok())
548 else {
549 continue;
550 };
551
552 let timestamp = msg_node
553 .get_attr("t")
554 .map(|v| v.as_str())
555 .and_then(|s| s.parse::<u64>().ok())
556 .unwrap_or(0);
557
558 let message_type = msg_node
559 .get_attr("type")
560 .map(|v| v.as_str())
561 .map(|s| NewsletterMessageType::from(s.as_ref()))
562 .unwrap_or(NewsletterMessageType::Text);
563
564 let is_sender = msg_node
565 .get_attr("is_sender")
566 .is_some_and(|v| v.as_str() == "true");
567
568 let message =
570 msg_node
571 .get_optional_child("plaintext")
572 .and_then(|pt| match pt.content.as_deref() {
573 Some(NodeContentRef::Bytes(bytes)) => wa::Message::decode(bytes.as_ref()).ok(),
574 _ => None,
575 });
576
577 let reactions = parse_reaction_counts(msg_node);
578
579 result.push(NewsletterMessage {
580 server_id,
581 timestamp,
582 message_type,
583 is_sender,
584 message,
585 reactions,
586 });
587 }
588
589 Ok(result)
590}
591
592#[cfg(test)]
593mod tests {
594 use super::*;
595 use wacore_binary::builder::NodeBuilder;
596
597 #[test]
598 fn test_missing_type_attribute_defaults_to_text() {
599 let response = NodeBuilder::new("iq")
600 .children([NodeBuilder::new("messages")
601 .children([NodeBuilder::new("message")
602 .attr("server_id", "42")
603 .attr("t", "1700000000")
604 .build()])
605 .build()])
606 .build();
607
608 let msgs = parse_newsletter_messages_response(&response.as_node_ref()).unwrap();
609 assert_eq!(msgs.len(), 1);
610 assert_eq!(msgs[0].message_type, NewsletterMessageType::Text);
611 }
612
613 #[test]
614 fn test_explicit_type_attribute_parsed() {
615 let response = NodeBuilder::new("iq")
616 .children([NodeBuilder::new("messages")
617 .children([NodeBuilder::new("message")
618 .attr("server_id", "1")
619 .attr("t", "1700000000")
620 .attr("type", "media")
621 .build()])
622 .build()])
623 .build();
624
625 let msgs = parse_newsletter_messages_response(&response.as_node_ref()).unwrap();
626 assert_eq!(msgs[0].message_type, NewsletterMessageType::Media);
627 }
628}