use crate::client::{Client, ClientError, NodeFilter};
use log::debug;
use std::time::Duration;
use thiserror::Error;
pub use wacore::media_retry::MediaRetryResult;
use wacore::media_retry::{
build_media_retry_receipt, encrypt_media_retry_receipt, parse_media_retry_notification,
};
use wacore_binary::{Jid, JidExt as _};
const MEDIA_RETRY_TIMEOUT: Duration = Duration::from_secs(30);
const MEDIA_REUPLOAD_CONCURRENCY: usize = 32;
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum MediaReuploadError {
#[error("{0}")]
Client(#[from] ClientError),
#[error("client is not logged in")]
NotLoggedIn,
#[error("invalid media reupload request: {0}")]
InvalidRequest(String),
#[error("media retry notification timed out")]
Timeout,
#[error("{0}")]
Internal(#[from] anyhow::Error),
}
pub struct MediaReuploadRequest<'a> {
pub msg_id: &'a str,
pub chat_jid: &'a Jid,
pub media_key: &'a [u8],
pub is_from_me: bool,
pub participant: Option<&'a Jid>,
}
pub struct MediaReupload<'a> {
client: &'a Client,
}
impl<'a> MediaReupload<'a> {
pub(crate) fn new(client: &'a Client) -> Self {
Self { client }
}
pub async fn request(
&self,
req: &MediaReuploadRequest<'_>,
) -> Result<MediaRetryResult, MediaReuploadError> {
if req.chat_jid.is_newsletter() {
return Err(MediaReuploadError::InvalidRequest(
"media reupload is not supported for newsletter messages".into(),
));
}
debug!(
"[media][rmr] Requesting media reupload for msg {} in chat {}",
req.msg_id, req.chat_jid
);
let (ciphertext, iv) = encrypt_media_retry_receipt(req.media_key, req.msg_id)?;
let device_snapshot = self.client.persistence_manager.get_device_snapshot();
let own_jid = device_snapshot
.pn
.as_ref()
.ok_or(MediaReuploadError::NotLoggedIn)?;
let waiter = self.client.wait_for_node(
NodeFilter::tag("notification")
.attr("type", "mediaretry")
.attr("id", req.msg_id),
);
let receipt_node = build_media_retry_receipt(
own_jid,
req.msg_id,
req.chat_jid,
req.is_from_me,
req.participant,
&ciphertext,
&iv,
);
self.client.send_node(receipt_node).await?;
debug!(
"[media][rmr] Sent server-error receipt for {}, waiting for response",
req.msg_id
);
let notification_node =
wacore::runtime::timeout(&*self.client.runtime, MEDIA_RETRY_TIMEOUT, waiter)
.await
.map_err(|_| MediaReuploadError::Timeout)?
.map_err(|_| {
MediaReuploadError::Internal(anyhow::anyhow!("media retry waiter cancelled"))
})?;
debug!(
"[media][rmr] Received mediaretry notification for {}",
req.msg_id
);
Ok(parse_media_retry_notification(
notification_node.get(),
req.media_key,
)?)
}
pub async fn request_many(
&self,
reqs: &[MediaReuploadRequest<'_>],
) -> Vec<Result<MediaRetryResult, MediaReuploadError>> {
use futures::StreamExt;
use std::collections::HashSet;
if reqs.is_empty() {
return Vec::new();
}
let mut results: Vec<Option<Result<MediaRetryResult, MediaReuploadError>>> =
(0..reqs.len()).map(|_| None).collect();
let mut seen: HashSet<&str> = HashSet::with_capacity(reqs.len());
let mut unique: Vec<usize> = Vec::with_capacity(reqs.len());
for (i, req) in reqs.iter().enumerate() {
if seen.insert(req.msg_id) {
unique.push(i);
} else {
results[i] = Some(Err(MediaReuploadError::InvalidRequest(format!(
"duplicate msg_id {} in batch",
req.msg_id
))));
}
}
let done: Vec<(usize, Result<MediaRetryResult, MediaReuploadError>)> =
futures::stream::iter(unique)
.map(|i| async move { (i, self.request(&reqs[i]).await) })
.buffer_unordered(MEDIA_REUPLOAD_CONCURRENCY)
.collect()
.await;
for (i, res) in done {
results[i] = Some(res);
}
results
.into_iter()
.map(|res| res.expect("every index is either a duplicate or fetched"))
.collect()
}
}
impl Client {
pub fn media_reupload(&self) -> MediaReupload<'_> {
MediaReupload::new(self)
}
}