New features
Events with RSVP (client.events())
A new Events feature creates WhatsApp event messages and collects encrypted RSVPs.
client.events().create(to, params)sends an event and returns(SendResult, message_secret). Like polls, the event carries a per-message secret; store it — responders’ RSVPs are encrypted against it.client.events().respond(chat, event_id, creator, secret, response, extra_guests)sends an encryptedGoing/NotGoing/Maybereply.wacore::event::{encrypt_event_response_with_secret, decrypt_event_response_with_secret}decrypt inbound RSVPs.
Polls::create_quiz)
client.polls().create_quiz(to, name, options, correct_index) sends a single-select quiz poll with one correct answer (correct_index is the 0-based index into options). It returns (SendResult, message_secret) just like create, and votes decrypt the same way. See create_quiz.
1:1 disappearing-message timer (Client::set_chat_disappearing_timer)
Turn disappearing messages on or off for a direct chat with client.set_chat_disappearing_timer(chat, duration_secs) (0 disables). It sends an EPHEMERAL_SETTING protocol message and is 1:1-only — use Groups::set_ephemeral for groups. See set_chat_disappearing_timer.
Save or rename contacts (ChatActions::save_contact)
client.chat_actions().save_contact(jid, full_name, first_name, save_on_primary_addressbook) writes a contact app-state mutation that syncs the name to your other linked devices. The contact id must be a bare phone-number JID; LIDs and device-specific JIDs are rejected. See save_contact.
Clear a chat’s messages (ChatActions::clear_chat)
client.chat_actions().clear_chat(jid, delete_starred, delete_media) clears a chat’s messages while keeping the chat, syncing across devices. Inbound clears from a linked device arrive as the new Event::ClearChatUpdate. See clear_chat.
Mute a contact’s status updates (ChatActions::set_user_status_mute)
client.chat_actions().set_user_status_mute(jid, muted) mutes/unmutes a contact, group, or channel’s status updates across devices. Inbound changes arrive as Event::UserStatusMuteUpdate. See set_user_status_mute.
Mute newsletter notifications (Newsletter::set_follower_mute / set_admin_mute)
Silence a channel’s follower- or admin-activity notifications via MEX:
set_follower_mute.
Message-secret encrypted edits (Client::edit_message_encrypted)
client.edit_message_encrypted(to, original_id, new_content) edits a message via the secret_encrypted_message (MESSAGE_EDIT) path instead of the plaintext protocolMessage edit. This is the form Community Announcement Groups require and what WhatsApp Web sends when message_edit_to_message_secret_sender_enabled is on. Newsletters are rejected — use Newsletter::edit_message. See edit_message_encrypted.
High-level media message builders (whatsapp_rust::media)
The new media module turns an UploadResponse into a ready-to-send wa::Message, so you no longer hand-assemble the CDN/crypto fields (url, direct_path, media_key, the two SHA-256 hashes, file_length, media_key_timestamp, streaming_sidecar):
image_message, video_message, document_message, and audio_message each take a typed options struct with sensible MIME defaults. See media message builders.
Device list from get_user_info
UserInfo now carries devices: Vec<u16> — the device IDs from the <devices version="2"> sublist the same usync query returns (device 0 is the primary). Empty when the server omits it.
offline flag on the Receipt event
Event::Receipt gains offline: bool, set when the receipt was drained from the server’s offline queue on reconnect rather than delivered live. See the receipt event structure.
Read-receipt status parity
Outgoing read receipts now match WhatsApp Web: newsletter reads send read-self, and status reads carry context="status" plus peer_participant_pn (the resolved LID→PN) for a LID author. No API change — mark_as_read handles it.
View-once sends emit <meta view_once="true"/>
View-once image/video/voice sends now carry the view_once meta attribute, matching WhatsApp Web so recipients render the one-time bubble. Members tagged for member labels also emit appdata / tag_reason meta attributes on group sends.
Breaking changes
Client::custom_enc_handlers field type changed (#792)
The pub custom_enc_handlers field type changed from Arc<async_lock::RwLock<HashMap<String, Arc<dyn EncHandler>>>> to std::sync::OnceLock<HashMap<String, Arc<dyn EncHandler>>>. Migration depends on how the field was used: code that read handlers via .read().await should switch to .get(); code that inserted handlers via .write().await.insert(...) must move those registrations to BotBuilder::with_enc_handler() before build() — runtime insertion is no longer possible.
get_participating returns HashMap<Jid, GroupMetadata>
Groups::get_participating is now keyed by Jid instead of String. Bind the key as a Jid; call .to_string() if you need the old string form.
download_from_params takes a DownloadParams struct
download_from_params and download_from_params_to_writer now take a single &DownloadParams instead of six positional arguments. Build one with DownloadParams::encrypted(direct_path, media_key, file_sha256, file_enc_sha256, file_length, media_type). DownloadParams implements Downloadable, so it also works directly with client.download(¶ms).
download_to_file removed
The dead, fully-buffering download_to_file is gone. Use download_to_writer (streaming, constant-memory) instead — it accepts any File/BufWriter and returns the writer seeked to 0.
set_description takes prev: Option<&str>
Groups::set_description now borrows the previous description id (Option<&str>) instead of taking an owned Option<String>. Pass Some("PREV_ID") or None.
mark_as_read / mark_as_played take &[&str]
mark_as_read and mark_as_played now take message_ids: &[&str] instead of Vec<String>, avoiding per-call allocations. Update call sites from vec!["ID".to_string()] to &["ID"].
Fixes & hardening
App-state anti-tampering parity: snapshot application now requires a valid snapshot MAC (no silent skip), rejects duplicate indices within a patch, and guards against version rollback — matching WhatsApp Web’s validation so a malicious or corrupted server snapshot can’t desync or tamper with state. Poll wire shape: poll creation now matches WhatsApp Web (pollContentType=TEXT, no vote metadata on the creation message), improving interop with other clients.
Companion device-identity (ADV) validation: fetched pre-key bundles are validated against the companion device-identity, rejecting bundles whose identity doesn’t check out before a session is built.
ADV account-key fallback from store (#790): Fixed a regression where contacts’ companion devices (WhatsApp Web / Desktop) stopped receiving messages after the ADV validation above was introduced. The server legitimately omits account_signature_key from a companion’s <device-identity> because the client already holds that key as the contact’s primary (device 0) identity in the Signal store — the prior validation rejected those bundles outright. The fix mirrors WA Web’s validateADVwithIdentityKey (e.accountSignatureKey || t): the in-blob key is preferred; when absent, the contact’s stored primary identity is loaded via Client::load_account_identity and used as the fallback. The validation result is now the three-state wacore::adv::AdvValidation (Valid / Invalid / NoAccountKey) instead of a boolean — an unverifiable-for-lack-of-key bundle is logged and kept rather than dropped, preserving the existing “device-identity absent” behaviour.
IQ errors keep error_type + backoff: failed IQ responses now surface the server’s error_type and server-directed backoff (seconds) instead of dropping them, so callers can honour retry hints.
wasm: the cache backend is now target-aware so the moka-cache default no longer breaks wasm32 builds.
EncHandler wasm portability (#793): EncHandler now uses MaybeSendSync as its supertrait and gates async_trait with ?Send on wasm32, matching the convention of EventHandler and SendContextResolver. Custom enc handlers that capture !Send JS handles now compile on the wasm32 port. No change on native — the blanket MaybeSendSync impl keeps Arc<dyn EncHandler> Send + Sync.
Networking traits wasm portability (#795): Transport, TransportFactory, and HttpClient now use MaybeSendSync as their supertrait, matching the convention established by EventHandler, SendContextResolver, and EncHandler. Custom transport and HTTP implementations that hold !Send JS handles (e.g. a browser WebSocket or fetch backing) now compile on the wasm32 port. No change on native — the blanket MaybeSendSync impl keeps Arc<dyn Transport> and Arc<dyn HttpClient> Send + Sync there.