Skip to main content

whatsapp_rust/features/
media_reupload.rs

1//! Media reupload feature: request the server to re-upload expired media.
2//!
3//! When a media download fails because the URL has expired, this feature
4//! sends a `<receipt type="server-error">` stanza and waits for a
5//! `<notification type="mediaretry">` response with a new `directPath`.
6//!
7//! Reference: WAWebRequestMediaReuploadManager (docs/captured-js/).
8
9use crate::client::{Client, ClientError, NodeFilter};
10use anyhow::Result;
11use log::debug;
12use std::time::Duration;
13pub use wacore::media_retry::MediaRetryResult;
14use wacore::media_retry::{
15    build_media_retry_receipt, encrypt_media_retry_receipt, parse_media_retry_notification,
16};
17use wacore_binary::jid::{Jid, JidExt as _};
18
19const MEDIA_RETRY_TIMEOUT: Duration = Duration::from_secs(30);
20
21/// Parameters for a media reupload request.
22pub struct MediaReuploadRequest<'a> {
23    /// The message ID containing the media.
24    pub msg_id: &'a str,
25    /// The chat JID where the message was received.
26    pub chat_jid: &'a Jid,
27    /// The raw media key bytes (32 bytes, from the message's `mediaKey` field).
28    pub media_key: &'a [u8],
29    /// Whether the message was sent by us.
30    pub is_from_me: bool,
31    /// For group/broadcast messages, the participant JID who sent the message.
32    pub participant: Option<&'a Jid>,
33}
34
35pub struct MediaReupload<'a> {
36    client: &'a Client,
37}
38
39impl<'a> MediaReupload<'a> {
40    pub(crate) fn new(client: &'a Client) -> Self {
41        Self { client }
42    }
43
44    /// Request the server to re-upload media for a message with an expired URL.
45    ///
46    /// Returns the new `directPath` on success, or an error variant indicating
47    /// why the reupload failed.
48    ///
49    /// # Protocol flow
50    /// 1. Encrypt `ServerErrorReceipt` protobuf with HKDF-derived key from media key
51    /// 2. Send `<receipt type="server-error">` with encrypted payload + `<rmr>` metadata
52    /// 3. Wait for `<notification type="mediaretry">` response
53    /// 4. Decrypt response and extract new `directPath`
54    pub async fn request(&self, req: &MediaReuploadRequest<'_>) -> Result<MediaRetryResult> {
55        // WA Web: ServerErrorReceiptJob rejects newsletter messages (no media keys).
56        anyhow::ensure!(
57            !req.chat_jid.is_newsletter(),
58            "media reupload is not supported for newsletter messages"
59        );
60
61        debug!(
62            "[media][rmr] Requesting media reupload for msg {} in chat {}",
63            req.msg_id, req.chat_jid
64        );
65
66        // Encrypt the ServerErrorReceipt
67        let (ciphertext, iv) = encrypt_media_retry_receipt(req.media_key, req.msg_id)?;
68
69        // Get own JID for the receipt's `to` attribute
70        let device_snapshot = self.client.persistence_manager.get_device_snapshot().await;
71        let own_jid = device_snapshot.pn.clone().ok_or(ClientError::NotLoggedIn)?;
72
73        // Register waiter BEFORE sending (to avoid race)
74        let waiter = self.client.wait_for_node(
75            NodeFilter::tag("notification")
76                .attr("type", "mediaretry")
77                .attr("id", req.msg_id),
78        );
79
80        // Build and send the receipt node
81        let receipt_node = build_media_retry_receipt(
82            &own_jid,
83            req.msg_id,
84            req.chat_jid,
85            req.is_from_me,
86            req.participant,
87            &ciphertext,
88            &iv,
89        );
90
91        self.client.send_node(receipt_node).await?;
92
93        debug!(
94            "[media][rmr] Sent server-error receipt for {}, waiting for response",
95            req.msg_id
96        );
97
98        // Wait for the mediaretry notification
99        let notification_node =
100            wacore::runtime::timeout(&*self.client.runtime, MEDIA_RETRY_TIMEOUT, waiter)
101                .await
102                .map_err(|_| anyhow::anyhow!("media retry notification timed out after 30s"))?
103                .map_err(|_| anyhow::anyhow!("media retry waiter cancelled"))?;
104
105        debug!(
106            "[media][rmr] Received mediaretry notification for {}",
107            req.msg_id
108        );
109
110        // Parse and decrypt the response
111        parse_media_retry_notification(&notification_node, req.media_key)
112    }
113}
114
115impl Client {
116    /// Access media reupload operations.
117    pub fn media_reupload(&self) -> MediaReupload<'_> {
118        MediaReupload::new(self)
119    }
120}