From 693d0cdadc59bc80f18df17171d0b4dc7e073023 Mon Sep 17 00:00:00 2001 From: "mintlify[bot]" <109931778+mintlify[bot]@users.noreply.github.com> Date: Wed, 18 Mar 2026 21:19:00 +0000 Subject: [PATCH] =?UTF-8?q?Document=20newsletter=20(channel)=20support=20?= =?UTF-8?q?=E2=80=94=20guide,=20API=20reference,=20and=20events?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Generated-By: mintlify-agent --- api/newsletter.mdx | 422 +++++++++++++++++++++++++++++++++++++++++ concepts/events.mdx | 51 +++++ docs.json | 2 + guides/newsletters.mdx | 262 +++++++++++++++++++++++++ 4 files changed, 737 insertions(+) create mode 100644 api/newsletter.mdx create mode 100644 guides/newsletters.mdx diff --git a/api/newsletter.mdx b/api/newsletter.mdx new file mode 100644 index 00000000..70cf9cc3 --- /dev/null +++ b/api/newsletter.mdx @@ -0,0 +1,422 @@ +--- +title: Newsletter +description: Newsletter (channel) operations — create, manage, subscribe, and send messages to WhatsApp channels +--- + +The `Newsletter` feature provides methods for managing WhatsApp newsletter channels, including creation, subscription management, messaging, reactions, and live updates. Newsletter operations use MEX (GraphQL) for metadata/management and IQ stanzas for message operations. + + +Newsletter messages are **plaintext** — they are not encrypted with the Signal protocol. + + +## Access + +Access newsletter operations through the client: + +```rust +let newsletter = client.newsletter(); +``` + +## Methods + +### list_subscribed + +List all newsletters the user is subscribed to. + +```rust +pub async fn list_subscribed(&self) -> Result, MexError> +``` + +**Returns:** +- `Vec` — List of subscribed newsletters + +**Example:** +```rust +let newsletters = client.newsletter().list_subscribed().await?; + +for nl in &newsletters { + println!("{}: {} ({} subscribers)", nl.jid, nl.name, nl.subscriber_count); +} +``` + +### get_metadata + +Fetch metadata for a newsletter by its JID. + +```rust +pub async fn get_metadata(&self, jid: &Jid) -> Result +``` + +**Parameters:** +- `jid` — Newsletter JID (server must be `newsletter`) + +**Returns:** +- `NewsletterMetadata` — Full newsletter metadata + +**Example:** +```rust +let metadata = client.newsletter().get_metadata(&newsletter_jid).await?; + +println!("Name: {}", metadata.name); +println!("Subscribers: {}", metadata.subscriber_count); +println!("Verified: {:?}", metadata.verification); +``` + +### get_metadata_by_invite + +Fetch metadata for a newsletter by its invite code. + +```rust +pub async fn get_metadata_by_invite( + &self, + invite_code: &str, +) -> Result +``` + +**Parameters:** +- `invite_code` — Newsletter invite code string + +**Returns:** +- `NewsletterMetadata` — Full newsletter metadata + +**Example:** +```rust +let metadata = client.newsletter() + .get_metadata_by_invite("ABC123") + .await?; + +println!("Found: {} ({})", metadata.name, metadata.jid); +``` + +### create + +Create a new newsletter. + +```rust +pub async fn create( + &self, + name: &str, + description: Option<&str>, +) -> Result +``` + +**Parameters:** +- `name` — Newsletter name +- `description` — Optional description + +**Returns:** +- `NewsletterMetadata` — Metadata of the newly created newsletter + +**Example:** +```rust +let created = client.newsletter() + .create("My Channel", Some("A description")) + .await?; + +println!("Created: {} ({})", created.name, created.jid); +``` + +### join + +Join (subscribe to) a newsletter. + +```rust +pub async fn join(&self, jid: &Jid) -> Result +``` + +**Parameters:** +- `jid` — Newsletter JID to join + +**Returns:** +- `NewsletterMetadata` — Metadata with the viewer's role set to `Subscriber` + +**Example:** +```rust +let joined = client.newsletter().join(&newsletter_jid).await?; + +println!("Joined '{}' as {:?}", joined.name, joined.role); +``` + +### leave + +Leave (unsubscribe from) a newsletter. + +```rust +pub async fn leave(&self, jid: &Jid) -> Result<(), MexError> +``` + +**Parameters:** +- `jid` — Newsletter JID to leave + +**Example:** +```rust +client.newsletter().leave(&newsletter_jid).await?; +``` + +### update + +Update a newsletter's name and/or description. + +```rust +pub async fn update( + &self, + jid: &Jid, + name: Option<&str>, + description: Option<&str>, +) -> Result +``` + +**Parameters:** +- `jid` — Newsletter JID +- `name` — New name, or `None` to keep the current name +- `description` — New description, or `None` to keep the current description + +**Returns:** +- `NewsletterMetadata` — Updated metadata + +**Example:** +```rust +let updated = client.newsletter() + .update(&newsletter_jid, Some("New Name"), None) + .await?; + +println!("Updated: {}", updated.name); +``` + +### send_message + +Send a message to a newsletter. Newsletter messages are plaintext (no Signal E2E encryption). + +```rust +pub async fn send_message( + &self, + jid: &Jid, + message: &wa::Message, +) -> Result +``` + +**Parameters:** +- `jid` — Newsletter JID +- `message` — Protobuf message to send + +**Returns:** +- `String` — Client-assigned message ID + +**Example:** +```rust +use waproto::whatsapp as wa; + +let message = wa::Message { + conversation: Some("Hello subscribers!".to_string()), + ..Default::default() +}; + +let msg_id = client.newsletter() + .send_message(&newsletter_jid, &message) + .await?; +``` + + +For media messages (images, videos, etc.), the media must be uploaded separately using the newsletter-specific upload endpoint before sending. Text messages work directly. + + +### send_reaction + +Send a reaction to a newsletter message. + +```rust +pub async fn send_reaction( + &self, + jid: &Jid, + server_id: u64, + reaction: &str, +) -> Result<(), anyhow::Error> +``` + +**Parameters:** +- `jid` — Newsletter JID +- `server_id` — Server-assigned ID of the message to react to +- `reaction` — Emoji code (e.g., `"👍"`, `"❤️"`), or empty string to remove + +**Example:** +```rust +// Add a reaction +client.newsletter() + .send_reaction(&newsletter_jid, server_id, "👍") + .await?; + +// Remove a reaction +client.newsletter() + .send_reaction(&newsletter_jid, server_id, "") + .await?; +``` + +### get_messages + +Fetch message history from a newsletter. + +```rust +pub async fn get_messages( + &self, + jid: &Jid, + count: u32, + before: Option, +) -> Result, anyhow::Error> +``` + +**Parameters:** +- `jid` — Newsletter JID +- `count` — Maximum number of messages to return +- `before` — If set, return messages before this `server_id` (for pagination) + +**Returns:** +- `Vec` — List of newsletter messages + +**Example:** +```rust +// Fetch latest 50 messages +let messages = client.newsletter() + .get_messages(&newsletter_jid, 50, None) + .await?; + +// Paginate backwards +if let Some(oldest) = messages.last() { + let older = client.newsletter() + .get_messages(&newsletter_jid, 50, Some(oldest.server_id)) + .await?; +} +``` + +### subscribe_live_updates + +Subscribe to live updates for a newsletter (reaction counts, message changes). + +```rust +pub async fn subscribe_live_updates( + &self, + jid: &Jid, +) -> Result +``` + +**Parameters:** +- `jid` — Newsletter JID + +**Returns:** +- `u64` — Subscription duration in seconds (typically 300) + +The server sends `Event::NewsletterLiveUpdate` events with updated reaction counts. You need to re-subscribe periodically when the duration expires. + +**Example:** +```rust +let duration = client.newsletter() + .subscribe_live_updates(&newsletter_jid) + .await?; + +println!("Subscribed for {}s", duration); +``` + +## Types + +### NewsletterMetadata + +Metadata for a newsletter channel. + +```rust +pub struct NewsletterMetadata { + pub jid: Jid, + pub name: String, + pub description: Option, + pub subscriber_count: u64, + pub verification: NewsletterVerification, + pub state: NewsletterState, + pub picture_url: Option, + pub preview_url: Option, + pub invite_code: Option, + pub role: Option, + pub creation_time: Option, +} +``` + +### NewsletterVerification + +```rust +pub enum NewsletterVerification { + Verified, + Unverified, +} +``` + +### NewsletterState + +```rust +pub enum NewsletterState { + Active, + Suspended, + Geosuspended, +} +``` + +### NewsletterRole + +The viewer's role in a newsletter. + +```rust +pub enum NewsletterRole { + Owner, + Admin, + Subscriber, + Guest, +} +``` + +### NewsletterMessage + +A message from a newsletter's history. + +```rust +pub struct NewsletterMessage { + /// Server-assigned message ID (monotonic, used for pagination cursors). + pub server_id: u64, + /// Message timestamp (Unix seconds). + pub timestamp: u64, + /// Message type ("text", "media", etc.). + pub message_type: String, + /// Whether the viewer is the sender. + pub is_sender: bool, + /// Decoded protobuf message (from plaintext bytes). + pub message: Option, + /// Reaction counts on this message. + pub reactions: Vec, +} +``` + +### NewsletterReactionCount + +A reaction count on a newsletter message. + +```rust +pub struct NewsletterReactionCount { + pub code: String, + pub count: u64, +} +``` + +## Error handling + +Metadata and management methods return `MexError`: + +```rust +pub enum MexError { + PayloadParsing(String), + // ... other variants +} +``` + +Message operations (`send_message`, `send_reaction`, `get_messages`, `subscribe_live_updates`) return `anyhow::Error`. + +```rust +match client.newsletter().create("Test", None).await { + Ok(metadata) => println!("Created: {}", metadata.name), + Err(e) => eprintln!("Failed: {}", e), +} +``` diff --git a/concepts/events.mdx b/concepts/events.mdx index 69c15fcd..77f56302 100644 --- a/concepts/events.mdx +++ b/concepts/events.mdx @@ -129,6 +129,9 @@ pub enum Event { DeviceListUpdate(DeviceListUpdate), BusinessStatusUpdate(BusinessStatusUpdate), + // Newsletter + NewsletterLiveUpdate(NewsletterLiveUpdate), + // Notification Updates DisappearingModeChanged(DisappearingModeChanged), } @@ -1034,6 +1037,54 @@ pub enum BusinessUpdateType { } ``` +## Newsletter Events + +### NewsletterLiveUpdate + +**Emitted:** When reaction counts change or messages are updated on a newsletter you're subscribed to (via `subscribe_live_updates`). + +```rust +#[derive(Debug, Clone, Serialize)] +pub struct NewsletterLiveUpdate { + pub newsletter_jid: Jid, + pub messages: Vec, +} + +#[derive(Debug, Clone, Serialize)] +pub struct NewsletterLiveUpdateMessage { + pub server_id: u64, + pub reactions: Vec, +} + +#[derive(Debug, Clone, Serialize)] +pub struct NewsletterLiveUpdateReaction { + pub code: String, + pub count: u64, +} +``` + +**Fields:** +- `newsletter_jid` — The newsletter channel this update is for +- `messages` — List of messages with updated reaction counts +- `server_id` — Server-assigned message ID +- `reactions` — Current reaction counts (emoji code and count) + +**Example:** +```rust +Event::NewsletterLiveUpdate(update) => { + println!("Newsletter {} updated:", update.newsletter_jid); + for msg in &update.messages { + for r in &msg.reactions { + println!(" Message {}: {} x{}", msg.server_id, r.code, r.count); + } + } +} +``` + + +You must call `client.newsletter().subscribe_live_updates(&jid)` to receive these events. The subscription has a limited duration (typically 300 seconds) and must be renewed periodically. + + ## Notification Events ### DisappearingModeChanged diff --git a/docs.json b/docs.json index 09c80ec0..28c079f1 100644 --- a/docs.json +++ b/docs.json @@ -47,6 +47,7 @@ "guides/receiving-messages", "guides/media-handling", "guides/group-management", + "guides/newsletters", "guides/custom-backends" ] }, @@ -92,6 +93,7 @@ "api/profile", "api/chat-actions", "api/mex", + "api/newsletter", "api/tctoken" ] }, diff --git a/guides/newsletters.mdx b/guides/newsletters.mdx new file mode 100644 index 00000000..fbbd09b7 --- /dev/null +++ b/guides/newsletters.mdx @@ -0,0 +1,262 @@ +--- +title: Newsletters (channels) +description: Learn how to create, manage, and interact with WhatsApp newsletter channels in whatsapp-rust +--- + +## Overview + +Newsletters (also called channels) are broadcast-style messaging in WhatsApp. Unlike groups, newsletter messages are **plaintext** (no Signal E2E encryption) and flow one-way from admins to subscribers. + +This guide covers creating newsletters, managing subscriptions, sending messages, and handling live updates. + +## Accessing the Newsletter API + +All newsletter operations are accessed through the `newsletter()` method: + +```rust +let newsletter = client.newsletter(); +``` + +See [Newsletter API reference](/api/newsletter) for the full API. + +## Creating a newsletter + +```rust +let created = client.newsletter() + .create("My Channel", Some("Channel description")) + .await?; + +println!("Created: {} ({})", created.name, created.jid); +println!("Invite code: {:?}", created.invite_code); +``` + +The returned `NewsletterMetadata` contains the channel's JID (with `@newsletter` server), name, subscriber count, and invite code. + +See [Newsletter API reference](/api/newsletter#create) for details. + +## Listing subscribed newsletters + +```rust +let newsletters = client.newsletter().list_subscribed().await?; + +for nl in &newsletters { + println!("{}: {} ({} subscribers)", + nl.jid, nl.name, nl.subscriber_count); +} +``` + +See [Newsletter API reference](/api/newsletter#list_subscribed) for details. + +## Fetching metadata + +### By JID + +```rust +let metadata = client.newsletter().get_metadata(&newsletter_jid).await?; + +println!("Name: {}", metadata.name); +println!("Description: {:?}", metadata.description); +println!("Subscribers: {}", metadata.subscriber_count); +println!("Verification: {:?}", metadata.verification); +println!("State: {:?}", metadata.state); +``` + +### By invite code + +```rust +let metadata = client.newsletter() + .get_metadata_by_invite("invite-code-here") + .await?; + +println!("Found: {} ({})", metadata.name, metadata.jid); +``` + +See [Newsletter API reference](/api/newsletter#get_metadata) for all metadata fields. + +## Joining and leaving + +### Join a newsletter + +```rust +let joined = client.newsletter().join(&newsletter_jid).await?; + +println!("Joined '{}' as {:?}", joined.name, joined.role); +``` + +### Leave a newsletter + +```rust +client.newsletter().leave(&newsletter_jid).await?; +``` + +See [Newsletter API reference](/api/newsletter#join) for details. + +## Updating a newsletter + +You can update the name and/or description of a newsletter you own: + +```rust +let updated = client.newsletter() + .update( + &newsletter_jid, + Some("New Channel Name"), + Some("Updated description"), + ) + .await?; + +println!("Updated: {}", updated.name); +``` + +Pass `None` for fields you don't want to change. + +See [Newsletter API reference](/api/newsletter#update) for details. + +## Sending messages + +Newsletter messages are plaintext — they bypass Signal encryption entirely. + +### Text messages + +```rust +use waproto::whatsapp as wa; + +let message = wa::Message { + conversation: Some("Hello subscribers!".to_string()), + ..Default::default() +}; + +let msg_id = client.newsletter() + .send_message(&newsletter_jid, &message) + .await?; + +println!("Sent message: {}", msg_id); +``` + + +For media messages (images, videos, etc.), you must upload the media separately using the newsletter-specific upload endpoint before sending. Text messages work directly. + + +### Reactions + +Send a reaction to a specific newsletter message using its `server_id`: + +```rust +// React with thumbs up +client.newsletter() + .send_reaction(&newsletter_jid, server_id, "👍") + .await?; + +// Remove reaction (empty string) +client.newsletter() + .send_reaction(&newsletter_jid, server_id, "") + .await?; +``` + +See [Newsletter API reference](/api/newsletter#send_message) for details. + +## Fetching message history + +Retrieve past messages with pagination support: + +```rust +// Fetch the latest 50 messages +let messages = client.newsletter() + .get_messages(&newsletter_jid, 50, None) + .await?; + +for msg in &messages { + println!("ID: {}, time: {}, type: {}", + msg.server_id, msg.timestamp, msg.message_type); + + if let Some(decoded) = &msg.message { + if let Some(text) = &decoded.conversation { + println!(" Text: {}", text); + } + } + + for reaction in &msg.reactions { + println!(" {} x{}", reaction.code, reaction.count); + } +} +``` + +### Pagination + +Use the `server_id` from a previous response to paginate backwards: + +```rust +// Get messages before a specific server_id +let older = client.newsletter() + .get_messages(&newsletter_jid, 20, Some(last_server_id)) + .await?; +``` + +See [Newsletter API reference](/api/newsletter#get_messages) for details. + +## Live updates + +Subscribe to real-time updates for a newsletter to receive reaction count changes: + +```rust +let duration = client.newsletter() + .subscribe_live_updates(&newsletter_jid) + .await?; + +println!("Subscribed for {}s", duration); +``` + +The server sends `NewsletterLiveUpdate` events with updated reaction counts. Handle them in your event handler: + +```rust +use wacore::types::events::Event; + +Event::NewsletterLiveUpdate(update) => { + println!("Newsletter {} updated:", update.newsletter_jid); + for msg in &update.messages { + println!(" Message {}: {:?}", msg.server_id, + msg.reactions.iter() + .map(|r| format!("{} x{}", r.code, r.count)) + .collect::>() + ); + } +} +``` + + +The subscription duration (typically 300 seconds) is returned by the server. You need to re-subscribe periodically to continue receiving live updates. + + +See [Events reference](/concepts/events#newsletterliveupdate) for the full event type. + +## Error handling + +Newsletter operations return `MexError` for metadata/management operations and `anyhow::Error` for message operations: + +```rust +use whatsapp_rust::features::mex::MexError; + +match client.newsletter().get_metadata(&jid).await { + Ok(metadata) => println!("Found: {}", metadata.name), + Err(MexError::PayloadParsing(msg)) => { + eprintln!("Parse error: {}", msg); + } + Err(e) => eprintln!("Error: {}", e), +} +``` + +## Key differences from groups + +| Feature | Newsletters | Groups | +|---------|------------|--------| +| Encryption | Plaintext | Signal E2E | +| Messaging | One-way (admins only) | All members | +| JID server | `@newsletter` | `@g.us` | +| Reactions | Emoji counts (aggregated) | Per-message | +| API backend | MEX (GraphQL) | IQ stanzas | + +## Next steps + +- [Newsletter API reference](/api/newsletter) — Full API details and types +- [Events](/concepts/events) — Handle newsletter live update events +- [Sending messages](/guides/sending-messages) — Send messages to groups and contacts +- [MEX API](/api/mex) — Understand the GraphQL layer used by newsletters