1use crate::client::Client;
8use crate::features::mex::{MexError, MexRequest};
9use prost::Message as ProtoMessage;
10use serde_json::json;
11use wacore::iq::newsletter::NEWSLETTER_XMLNS;
12use wacore::request::InfoQuery;
13use wacore_binary::builder::NodeBuilder;
14use wacore_binary::jid::Jid;
15use wacore_binary::node::{Node, NodeContent};
16use waproto::whatsapp as wa;
17
18#[derive(Debug, Clone)]
22pub enum NewsletterVerification {
23 Verified,
24 Unverified,
25}
26
27#[derive(Debug, Clone)]
29pub enum NewsletterState {
30 Active,
31 Suspended,
32 Geosuspended,
33}
34
35#[derive(Debug, Clone)]
37pub enum NewsletterRole {
38 Owner,
39 Admin,
40 Subscriber,
41 Guest,
42}
43
44#[derive(Debug, Clone)]
46pub struct NewsletterMetadata {
47 pub jid: Jid,
48 pub name: String,
49 pub description: Option<String>,
50 pub subscriber_count: u64,
51 pub verification: NewsletterVerification,
52 pub state: NewsletterState,
53 pub picture_url: Option<String>,
54 pub preview_url: Option<String>,
55 pub invite_code: Option<String>,
56 pub role: Option<NewsletterRole>,
57 pub creation_time: Option<u64>,
58}
59
60#[derive(Debug, Clone)]
62pub struct NewsletterReactionCount {
63 pub code: String,
64 pub count: u64,
65}
66
67#[derive(Debug, Clone)]
69pub struct NewsletterMessage {
70 pub server_id: u64,
72 pub timestamp: u64,
74 pub message_type: String,
76 pub is_sender: bool,
78 pub message: Option<wa::Message>,
80 pub reactions: Vec<NewsletterReactionCount>,
82}
83
84pub struct Newsletter<'a> {
86 client: &'a Client,
87}
88
89impl<'a> Newsletter<'a> {
90 pub(crate) fn new(client: &'a Client) -> Self {
91 Self { client }
92 }
93
94 pub async fn list_subscribed(&self) -> Result<Vec<NewsletterMetadata>, MexError> {
96 let response = self
97 .client
98 .mex()
99 .query(MexRequest {
100 doc_id: wacore::iq::newsletter::mex_docs::LIST_SUBSCRIBED,
101 variables: json!({}),
102 })
103 .await?;
104
105 let data = response
106 .data
107 .ok_or_else(|| MexError::PayloadParsing("missing data".into()))?;
108 let newsletters = data["xwa2_newsletter_subscribed"]
109 .as_array()
110 .ok_or_else(|| {
111 MexError::PayloadParsing("missing xwa2_newsletter_subscribed array".into())
112 })?;
113
114 newsletters.iter().map(parse_newsletter_metadata).collect()
115 }
116
117 pub async fn get_metadata(&self, jid: &Jid) -> Result<NewsletterMetadata, MexError> {
119 let response = self
120 .client
121 .mex()
122 .query(MexRequest {
123 doc_id: wacore::iq::newsletter::mex_docs::FETCH_METADATA,
124 variables: json!({
125 "input": {
126 "key": jid.to_string(),
127 "type": "JID",
128 "view_role": "GUEST"
129 },
130 "fetch_viewer_metadata": true,
131 "fetch_full_image": true,
132 "fetch_creation_time": true
133 }),
134 })
135 .await?;
136
137 let data = response
138 .data
139 .ok_or_else(|| MexError::PayloadParsing("missing data".into()))?;
140 let newsletter = &data["xwa2_newsletter"];
141 if newsletter.is_null() {
142 return Err(MexError::PayloadParsing(format!(
143 "newsletter not found: {}",
144 jid
145 )));
146 }
147 parse_newsletter_metadata(newsletter)
148 }
149
150 pub async fn create(
154 &self,
155 name: &str,
156 description: Option<&str>,
157 ) -> Result<NewsletterMetadata, MexError> {
158 let mut input = json!({ "name": name });
159 if let Some(desc) = description {
160 input["description"] = json!(desc);
161 }
162
163 let response = self
164 .client
165 .mex()
166 .mutate(MexRequest {
167 doc_id: wacore::iq::newsletter::mex_docs::CREATE,
168 variables: json!({ "input": input }),
169 })
170 .await?;
171
172 let data = response
173 .data
174 .ok_or_else(|| MexError::PayloadParsing("missing data".into()))?;
175 let newsletter = &data["xwa2_newsletter_create"];
176 if newsletter.is_null() {
177 return Err(MexError::PayloadParsing(
178 "newsletter creation failed".into(),
179 ));
180 }
181 parse_newsletter_metadata(newsletter)
182 }
183
184 pub async fn join(&self, jid: &Jid) -> Result<NewsletterMetadata, MexError> {
188 let response = self
189 .client
190 .mex()
191 .mutate(MexRequest {
192 doc_id: wacore::iq::newsletter::mex_docs::JOIN,
193 variables: json!({
194 "newsletter_id": jid.to_string()
195 }),
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_join_v2"];
203 if newsletter.is_null() {
204 return Err(MexError::PayloadParsing(format!(
205 "failed to join newsletter: {}",
206 jid
207 )));
208 }
209 parse_newsletter_metadata(newsletter)
210 }
211
212 pub async fn leave(&self, jid: &Jid) -> Result<(), MexError> {
214 let response = self
215 .client
216 .mex()
217 .mutate(MexRequest {
218 doc_id: wacore::iq::newsletter::mex_docs::LEAVE,
219 variables: json!({
220 "newsletter_id": jid.to_string()
221 }),
222 })
223 .await?;
224
225 let data = response
226 .data
227 .ok_or_else(|| MexError::PayloadParsing("missing data".into()))?;
228 if data["xwa2_newsletter_leave_v2"].is_null() {
229 return Err(MexError::PayloadParsing(format!(
230 "failed to leave newsletter: {}",
231 jid
232 )));
233 }
234 Ok(())
235 }
236
237 pub async fn update(
239 &self,
240 jid: &Jid,
241 name: Option<&str>,
242 description: Option<&str>,
243 ) -> Result<NewsletterMetadata, MexError> {
244 let mut updates = json!({});
245 if let Some(name) = name {
246 updates["name"] = json!(name);
247 }
248 if let Some(desc) = description {
249 updates["description"] = json!(desc);
250 }
251
252 let response = self
253 .client
254 .mex()
255 .mutate(MexRequest {
256 doc_id: wacore::iq::newsletter::mex_docs::UPDATE,
257 variables: json!({
258 "newsletter_id": jid.to_string(),
259 "updates": updates
260 }),
261 })
262 .await?;
263
264 let data = response
265 .data
266 .ok_or_else(|| MexError::PayloadParsing("missing data".into()))?;
267 let newsletter = &data["xwa2_newsletter_update"];
268 if newsletter.is_null() {
269 return Err(MexError::PayloadParsing(format!(
270 "failed to update newsletter: {}",
271 jid
272 )));
273 }
274 parse_newsletter_metadata(newsletter)
275 }
276
277 pub async fn get_metadata_by_invite(
279 &self,
280 invite_code: &str,
281 ) -> Result<NewsletterMetadata, MexError> {
282 let response = self
283 .client
284 .mex()
285 .query(MexRequest {
286 doc_id: wacore::iq::newsletter::mex_docs::FETCH_METADATA,
287 variables: json!({
288 "input": {
289 "key": invite_code,
290 "type": "INVITE",
291 "view_role": "GUEST"
292 },
293 "fetch_viewer_metadata": true,
294 "fetch_full_image": true,
295 "fetch_creation_time": true
296 }),
297 })
298 .await?;
299
300 let data = response
301 .data
302 .ok_or_else(|| MexError::PayloadParsing("missing data".into()))?;
303 let newsletter = &data["xwa2_newsletter"];
304 if newsletter.is_null() {
305 return Err(MexError::PayloadParsing(format!(
306 "newsletter not found for invite: {}",
307 invite_code
308 )));
309 }
310 parse_newsletter_metadata(newsletter)
311 }
312
313 pub async fn subscribe_live_updates(&self, jid: &Jid) -> Result<u64, anyhow::Error> {
321 let iq = InfoQuery::set(
322 NEWSLETTER_XMLNS,
323 jid.clone(),
324 Some(NodeContent::Nodes(vec![
325 NodeBuilder::new("live_updates").build(),
326 ])),
327 );
328
329 let response = self.client.send_iq(iq).await?;
330 let duration = response
331 .get_optional_child("live_updates")
332 .and_then(|n| n.attrs.get("duration"))
333 .map(|v| v.as_str())
334 .and_then(|s| s.parse::<u64>().ok())
335 .unwrap_or(300);
336
337 Ok(duration)
338 }
339
340 pub async fn send_message(
351 &self,
352 jid: &Jid,
353 message: &wa::Message,
354 ) -> Result<String, anyhow::Error> {
355 let request_id = self.client.generate_message_id().await;
356 let encoded = message.encode_to_vec();
357
358 let stanza = NodeBuilder::new("message")
359 .attr("to", jid.clone())
360 .attr("type", "text")
361 .attr("id", &request_id)
362 .children([NodeBuilder::new("plaintext").bytes(encoded).build()])
363 .build();
364
365 self.client.send_node(stanza).await?;
366 Ok(request_id)
367 }
368
369 pub async fn send_reaction(
374 &self,
375 jid: &Jid,
376 server_id: u64,
377 reaction: &str,
378 ) -> Result<(), anyhow::Error> {
379 let request_id = self.client.generate_message_id().await;
380
381 let stanza = NodeBuilder::new("message")
382 .attr("to", jid.clone())
383 .attr("type", "reaction")
384 .attr("id", &request_id)
385 .attr("server_id", server_id.to_string())
386 .children([NodeBuilder::new("reaction").attr("code", reaction).build()])
387 .build();
388
389 self.client.send_node(stanza).await?;
390 Ok(())
391 }
392
393 pub async fn get_messages(
398 &self,
399 jid: &Jid,
400 count: u32,
401 before: Option<u64>,
402 ) -> Result<Vec<NewsletterMessage>, anyhow::Error> {
403 let mut messages_node = NodeBuilder::new("messages").attr("count", count.to_string());
404 if let Some(before_id) = before {
405 messages_node = messages_node.attr("before", before_id.to_string());
406 }
407
408 let iq = InfoQuery::get(
409 NEWSLETTER_XMLNS,
410 jid.clone(),
411 Some(NodeContent::Nodes(vec![messages_node.build()])),
412 );
413
414 let response = self.client.send_iq(iq).await?;
415 parse_newsletter_messages_response(&response)
416 }
417}
418
419impl Client {
420 #[inline]
422 pub fn newsletter(&self) -> Newsletter<'_> {
423 Newsletter::new(self)
424 }
425}
426
427fn parse_newsletter_metadata(value: &serde_json::Value) -> Result<NewsletterMetadata, MexError> {
430 let jid_str = value["id"]
431 .as_str()
432 .ok_or_else(|| MexError::PayloadParsing("missing newsletter id".into()))?;
433 let jid: Jid = jid_str
434 .parse()
435 .map_err(|e| MexError::PayloadParsing(format!("invalid newsletter JID: {e}")))?;
436
437 let thread = &value["thread_metadata"];
438
439 let name = thread["name"]["text"].as_str().unwrap_or("").to_string();
440 let description = thread["description"]["text"]
441 .as_str()
442 .filter(|s| !s.is_empty())
443 .map(|s| s.to_string());
444
445 let subscriber_count = thread["subscribers_count"]
446 .as_str()
447 .and_then(|s| s.parse::<u64>().ok())
448 .unwrap_or(0);
449
450 let verification = match thread["verification"].as_str() {
451 Some("VERIFIED") => NewsletterVerification::Verified,
452 _ => NewsletterVerification::Unverified,
453 };
454
455 let state = match value["state"]["type"].as_str() {
456 Some("suspended") => NewsletterState::Suspended,
457 Some("geosuspended") => NewsletterState::Geosuspended,
458 _ => NewsletterState::Active,
459 };
460
461 let picture_url = thread["picture"]["direct_path"]
462 .as_str()
463 .map(|s| s.to_string());
464 let preview_url = thread["preview"]["direct_path"]
465 .as_str()
466 .map(|s| s.to_string());
467 let invite_code = thread["invite"].as_str().map(|s| s.to_string());
468
469 let creation_time = thread["creation_time"]
470 .as_str()
471 .and_then(|s| s.parse::<u64>().ok());
472
473 let role = value["viewer_metadata"]["role"]
474 .as_str()
475 .and_then(|r| match r {
476 "owner" => Some(NewsletterRole::Owner),
477 "admin" => Some(NewsletterRole::Admin),
478 "subscriber" => Some(NewsletterRole::Subscriber),
479 "guest" => Some(NewsletterRole::Guest),
480 _ => None,
481 });
482
483 Ok(NewsletterMetadata {
484 jid,
485 name,
486 description,
487 subscriber_count,
488 verification,
489 state,
490 picture_url,
491 preview_url,
492 invite_code,
493 role,
494 creation_time,
495 })
496}
497
498pub(crate) fn parse_reaction_counts(node: &Node) -> Vec<NewsletterReactionCount> {
503 let mut reactions = Vec::new();
504 if let Some(reactions_node) = node.get_optional_child("reactions")
505 && let Some(children) = reactions_node.children()
506 {
507 for r in children.iter().filter(|n| n.tag.as_ref() == "reaction") {
508 let Some(code) = r
509 .attrs
510 .get("code")
511 .map(|v| v.as_str().into_owned())
512 .filter(|s| !s.is_empty())
513 else {
514 continue;
515 };
516 let count = r
517 .attrs
518 .get("count")
519 .map(|v| v.as_str())
520 .and_then(|s| s.parse::<u64>().ok())
521 .unwrap_or(0);
522 reactions.push(NewsletterReactionCount { code, count });
523 }
524 }
525 reactions
526}
527
528fn parse_newsletter_messages_response(
542 response: &Node,
543) -> Result<Vec<NewsletterMessage>, anyhow::Error> {
544 let messages_node = response
546 .get_optional_child("messages")
547 .ok_or_else(|| anyhow::anyhow!("missing <messages> in newsletter response"))?;
548
549 let children = match messages_node.children() {
550 Some(c) => c,
551 None => return Ok(vec![]),
552 };
553
554 let mut result = Vec::with_capacity(children.len());
555 for msg_node in children.iter().filter(|n| n.tag.as_ref() == "message") {
556 let Some(server_id) = msg_node
558 .attrs
559 .get("server_id")
560 .map(|v| v.as_str())
561 .and_then(|s| s.parse::<u64>().ok())
562 else {
563 continue;
564 };
565
566 let timestamp = msg_node
567 .attrs
568 .get("t")
569 .map(|v| v.as_str())
570 .and_then(|s| s.parse::<u64>().ok())
571 .unwrap_or(0);
572
573 let message_type = msg_node
574 .attrs
575 .get("type")
576 .map(|v| v.as_str().into_owned())
577 .unwrap_or_default();
578
579 let is_sender = msg_node.attrs.get("is_sender").is_some_and(|v| v == "true");
580
581 let message = msg_node
583 .get_optional_child("plaintext")
584 .and_then(|pt| match &pt.content {
585 Some(NodeContent::Bytes(bytes)) => wa::Message::decode(bytes.as_slice()).ok(),
586 _ => None,
587 });
588
589 let reactions = parse_reaction_counts(msg_node);
590
591 result.push(NewsletterMessage {
592 server_id,
593 timestamp,
594 message_type,
595 is_sender,
596 message,
597 reactions,
598 });
599 }
600
601 Ok(result)
602}