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