Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
pub use wacore::{iq::privacy as privacy_settings, proto_helpers, store::traits};
pub use wacore::{
iq::privacy as privacy_settings, proto_helpers, sticker_pack, store::traits, webp,
};
pub use wacore_binary::builder::NodeBuilder;
pub use wacore_binary::jid::Jid;
pub use waproto;
Expand Down Expand Up @@ -43,6 +45,7 @@ pub mod socket;
pub mod store;
pub mod transport;
pub mod upload;
pub use upload::UploadOptions;

pub mod pdo;
pub mod prekeys;
Expand Down
57 changes: 54 additions & 3 deletions src/upload.rs
Original file line number Diff line number Diff line change
Expand Up @@ -241,21 +241,72 @@ pub struct UploadResponse {
pub media_key_timestamp: i64,
}

impl From<UploadResponse> for wacore::sticker_pack::MediaUploadInfo {
fn from(r: UploadResponse) -> Self {
Self::new(
r.direct_path,
r.media_key,
r.file_sha256,
r.file_enc_sha256,
r.file_length,
r.media_key_timestamp,
)
}
}

#[derive(Deserialize)]
struct RawUploadResponse {
url: String,
direct_path: String,
}

#[non_exhaustive]
#[derive(Default, Clone)]
pub struct UploadOptions {
/// Reuse an existing media key instead of generating a fresh one.
pub media_key: Option<Vec<u8>>,
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

impl std::fmt::Debug for UploadOptions {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("UploadOptions")
.field("media_key", &self.media_key.as_ref().map(|_| "<redacted>"))
.finish()
}
}

impl UploadOptions {
pub fn new() -> Self {
Self::default()
}

pub fn with_media_key(mut self, key: Vec<u8>) -> Self {
self.media_key = Some(key);
self
}
}

impl Client {
/// Encrypts and uploads media to WhatsApp's CDN with a fresh key.
/// Encrypts and uploads media to WhatsApp's CDN.
///
/// Only needed for new or modified media. To forward existing media unchanged,
/// reuse the original message's CDN fields directly, no round-trip required.
pub async fn upload(&self, data: Vec<u8>, media_type: MediaType) -> Result<UploadResponse> {
pub async fn upload(
&self,
data: Vec<u8>,
media_type: MediaType,
options: UploadOptions,
) -> Result<UploadResponse> {
let file_length = data.len() as u64;
let enc = wacore::runtime::blocking(&*self.runtime, move || {
wacore::upload::encrypt_media(&data, media_type)
let key_ref = match &options.media_key {
Some(k) => Some(
<&[u8; 32]>::try_from(k.as_slice())
.map_err(|_| anyhow!("media_key must be exactly 32 bytes"))?,
),
None => None,
};
wacore::upload::encrypt_media_with_key(&data, media_type, key_ref)
})
.await?;

Expand Down
56 changes: 34 additions & 22 deletions tests/e2e/tests/media.rs
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,10 @@ async fn test_upload_image() -> anyhow::Result<()> {

let data = vec![0xFFu8, 0xD8, 0xFF, 0xE0, 0x00, 0x10]; // fake JPEG header

let resp = client.client.upload(data.clone(), MediaType::Image).await?;
let resp = client
.client
.upload(data.clone(), MediaType::Image, Default::default())
.await?;

info!(
"Upload response: url={}, direct_path={}",
Expand Down Expand Up @@ -131,7 +134,10 @@ async fn test_upload_video() -> anyhow::Result<()> {
let client = TestClient::connect("e2e_upload_vid").await?;

let data = vec![0u8; 64]; // fake video bytes
let resp = client.client.upload(data.clone(), MediaType::Video).await?;
let resp = client
.client
.upload(data.clone(), MediaType::Video, Default::default())
.await?;

assert!(!resp.url.is_empty());
assert!(!resp.direct_path.is_empty());
Expand All @@ -150,7 +156,7 @@ async fn test_upload_document() -> anyhow::Result<()> {
let data = b"%PDF-1.4 fake pdf content for testing purposes".to_vec();
let resp = client
.client
.upload(data.clone(), MediaType::Document)
.upload(data.clone(), MediaType::Document, Default::default())
.await?;

assert!(!resp.url.is_empty());
Expand All @@ -168,7 +174,10 @@ async fn test_upload_audio() -> anyhow::Result<()> {
let client = TestClient::connect("e2e_upload_aud").await?;

let data = vec![0u8; 64]; // fake audio bytes
let resp = client.client.upload(data.clone(), MediaType::Audio).await?;
let resp = client
.client
.upload(data.clone(), MediaType::Audio, Default::default())
.await?;

assert!(!resp.url.is_empty());
assert!(!resp.direct_path.is_empty());
Expand All @@ -189,7 +198,7 @@ async fn test_upload_then_download_image() -> anyhow::Result<()> {
let original = b"JPEG image content for round-trip test".to_vec();
let upload = client
.client
.upload(original.clone(), MediaType::Image)
.upload(original.clone(), MediaType::Image, Default::default())
.await?;

info!("Uploaded: direct_path={}", upload.direct_path);
Expand Down Expand Up @@ -225,7 +234,7 @@ async fn test_upload_then_download_video() -> anyhow::Result<()> {
let original = vec![0xAB; 64]; // fake video
let upload = client
.client
.upload(original.clone(), MediaType::Video)
.upload(original.clone(), MediaType::Video, Default::default())
.await?;

let downloaded = client
Expand Down Expand Up @@ -255,7 +264,7 @@ async fn test_upload_then_download_document() -> anyhow::Result<()> {
let original = b"PDF document content for testing".to_vec();
let upload = client
.client
.upload(original.clone(), MediaType::Document)
.upload(original.clone(), MediaType::Document, Default::default())
.await?;

let downloaded = client
Expand Down Expand Up @@ -285,7 +294,7 @@ async fn test_upload_then_download_via_downloadable_trait() -> anyhow::Result<()
let original = b"Testing Downloadable trait with ImageMessage".to_vec();
let upload = client
.client
.upload(original.clone(), MediaType::Image)
.upload(original.clone(), MediaType::Image, Default::default())
.await?;

// Build an ImageMessage (which implements Downloadable)
Expand Down Expand Up @@ -319,7 +328,7 @@ async fn test_upload_then_download_to_writer() -> anyhow::Result<()> {
let original = b"Streaming download test content".to_vec();
let upload = client
.client
.upload(original.clone(), MediaType::Image)
.upload(original.clone(), MediaType::Image, Default::default())
.await?;

let cursor = std::io::Cursor::new(Vec::<u8>::new());
Expand Down Expand Up @@ -362,7 +371,7 @@ async fn test_send_image_message() -> anyhow::Result<()> {
let original = b"Image bytes sent from A to B".to_vec();
let upload = client_a
.client
.upload(original.clone(), MediaType::Image)
.upload(original.clone(), MediaType::Image, Default::default())
.await?;

// A sends the image to B
Expand Down Expand Up @@ -428,7 +437,7 @@ async fn test_send_video_message() -> anyhow::Result<()> {
let original = vec![0xBB; 64];
let upload = client_a
.client
.upload(original.clone(), MediaType::Video)
.upload(original.clone(), MediaType::Video, Default::default())
.await?;

let msg = build_video_message(&upload, Some("Cool video"), 15);
Expand Down Expand Up @@ -478,7 +487,7 @@ async fn test_send_document_message() -> anyhow::Result<()> {
let original = b"Important document content".to_vec();
let upload = client_a
.client
.upload(original.clone(), MediaType::Document)
.upload(original.clone(), MediaType::Document, Default::default())
.await?;

let msg = build_document_message(&upload, "report.pdf", "application/pdf");
Expand Down Expand Up @@ -527,7 +536,7 @@ async fn test_send_audio_message() -> anyhow::Result<()> {
let original = vec![0xCC; 64];
let upload = client_a
.client
.upload(original.clone(), MediaType::Audio)
.upload(original.clone(), MediaType::Audio, Default::default())
.await?;

let msg = build_audio_message(&upload, false, 30);
Expand Down Expand Up @@ -576,7 +585,7 @@ async fn test_send_ptt_voice_message() -> anyhow::Result<()> {
let original = vec![0xDD; 64];
let upload = client_a
.client
.upload(original.clone(), MediaType::Audio)
.upload(original.clone(), MediaType::Audio, Default::default())
.await?;

let msg = build_audio_message(&upload, true, 5);
Expand Down Expand Up @@ -635,7 +644,7 @@ async fn test_send_image_bidirectional() -> anyhow::Result<()> {
let data_a = b"Image from A to B".to_vec();
let upload_a = client_a
.client
.upload(data_a.clone(), MediaType::Image)
.upload(data_a.clone(), MediaType::Image, Default::default())
.await?;
let msg_a = build_image_message(&upload_a, Some("From A"));
client_a.client.send_message(jid_b.clone(), msg_a).await?;
Expand All @@ -661,7 +670,7 @@ async fn test_send_image_bidirectional() -> anyhow::Result<()> {
let data_b = b"Image from B to A".to_vec();
let upload_b = client_b
.client
.upload(data_b.clone(), MediaType::Image)
.upload(data_b.clone(), MediaType::Image, Default::default())
.await?;
let msg_b = build_image_message(&upload_b, Some("From B"));
client_b.client.send_message(jid_a.clone(), msg_b).await?;
Expand Down Expand Up @@ -708,7 +717,7 @@ async fn test_send_multiple_media_types() -> anyhow::Result<()> {
let img_data = b"image data for multi-type test".to_vec();
let img_upload = client_a
.client
.upload(img_data.clone(), MediaType::Image)
.upload(img_data.clone(), MediaType::Image, Default::default())
.await?;
let img_msg = build_image_message(&img_upload, Some("Photo"));
client_a.client.send_message(jid_b.clone(), img_msg).await?;
Expand All @@ -732,7 +741,7 @@ async fn test_send_multiple_media_types() -> anyhow::Result<()> {
let doc_data = b"document data for multi-type test".to_vec();
let doc_upload = client_a
.client
.upload(doc_data.clone(), MediaType::Document)
.upload(doc_data.clone(), MediaType::Document, Default::default())
.await?;
let doc_msg = build_document_message(&doc_upload, "file.txt", "text/plain");
client_a.client.send_message(jid_b.clone(), doc_msg).await?;
Expand All @@ -756,7 +765,7 @@ async fn test_send_multiple_media_types() -> anyhow::Result<()> {
let aud_data = b"audio data for multi-type test".to_vec();
let aud_upload = client_a
.client
.upload(aud_data.clone(), MediaType::Audio)
.upload(aud_data.clone(), MediaType::Audio, Default::default())
.await?;
let aud_msg = build_audio_message(&aud_upload, true, 10);
client_a.client.send_message(jid_b.clone(), aud_msg).await?;
Expand Down Expand Up @@ -793,7 +802,7 @@ async fn test_upload_download_large_file() -> anyhow::Result<()> {
let original = vec![0x42u8; 256];
let upload = client
.client
.upload(original.clone(), MediaType::Document)
.upload(original.clone(), MediaType::Document, Default::default())
.await?;

let downloaded = client
Expand Down Expand Up @@ -826,7 +835,10 @@ async fn test_multiple_uploads_reuse_media_conn() -> anyhow::Result<()> {
// Upload multiple files in sequence - media_conn should be cached
for i in 0..3 {
let data = format!("Upload number {i}").into_bytes();
let resp = client.client.upload(data.clone(), MediaType::Image).await?;
let resp = client
.client
.upload(data.clone(), MediaType::Image, Default::default())
.await?;
info!("Upload {i}: direct_path={}", resp.direct_path);
assert!(!resp.direct_path.is_empty());

Expand Down Expand Up @@ -868,7 +880,7 @@ async fn test_send_image_no_caption() -> anyhow::Result<()> {
let original = b"Image without caption".to_vec();
let upload = client_a
.client
.upload(original.clone(), MediaType::Image)
.upload(original.clone(), MediaType::Image, Default::default())
.await?;

let msg = build_image_message(&upload, None);
Expand Down
5 changes: 4 additions & 1 deletion tests/e2e/tests/newsletter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -267,7 +267,10 @@ async fn test_newsletter_send_media_message() -> anyhow::Result<()> {

// Upload a fake image
let data = vec![0xFFu8, 0xD8, 0xFF, 0xE0, 0x00, 0x10];
let upload = client.client.upload(data, MediaType::Image).await?;
let upload = client
.client
.upload(data, MediaType::Image, Default::default())
.await?;

// Build and send an image message
let message = wa::Message {
Expand Down
10 changes: 10 additions & 0 deletions wacore/src/download.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ pub enum MediaDecryptionError {
Other(#[from] anyhow::Error),
}

#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MediaType {
Image,
Expand All @@ -33,6 +34,7 @@ pub enum MediaType {
AppState,
Sticker,
StickerPack,
StickerPackThumbnail,
LinkThumbnail,
/// Product catalog image — unencrypted, uploads to `/product/image`.
/// WA Web: CreateMediaKeys.js throws for this type (no encryption).
Expand All @@ -50,6 +52,7 @@ impl MediaType {
MediaType::AppState => "WhatsApp App State Keys",
MediaType::Sticker => "WhatsApp Image Keys",
MediaType::StickerPack => "WhatsApp Sticker Pack Keys",
MediaType::StickerPackThumbnail => "WhatsApp Sticker Pack Thumbnail Keys",
MediaType::LinkThumbnail => "WhatsApp Link Thumbnail Keys",
// Unencrypted: app_info unused, but keep a value for the type system.
MediaType::ProductCatalogImage => "WhatsApp Image Keys",
Expand All @@ -67,6 +70,7 @@ impl MediaType {
MediaType::History => "md-msg-hist",
MediaType::AppState => "md-app-state",
MediaType::StickerPack => "sticker-pack",
MediaType::StickerPackThumbnail => "thumbnail-sticker-pack",
MediaType::LinkThumbnail => "thumbnail-link",
MediaType::ProductCatalogImage => "product-catalog-image",
}
Expand All @@ -82,6 +86,7 @@ impl MediaType {
MediaType::History => "/mms/md-msg-hist",
MediaType::AppState => "/mms/md-app-state",
MediaType::StickerPack => "/mms/sticker-pack",
MediaType::StickerPackThumbnail => "/mms/thumbnail-sticker-pack",
MediaType::LinkThumbnail => "/mms/thumbnail-link",
MediaType::ProductCatalogImage => "/product/image",
}
Expand Down Expand Up @@ -196,6 +201,11 @@ impl_downloadable!(
);
impl_downloadable!(wa::message::AudioMessage, MediaType::Audio, file_length);
impl_downloadable!(wa::message::StickerMessage, MediaType::Sticker, file_length);
impl_downloadable!(
wa::message::StickerPackMessage,
MediaType::StickerPack,
file_length
);
impl_downloadable!(ExternalBlobReference, MediaType::AppState, file_size_bytes);
impl_downloadable!(HistorySyncNotification, MediaType::History, file_length);

Expand Down
5 changes: 5 additions & 0 deletions wacore/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,15 @@ pub mod runtime;
pub mod send;
pub mod session;
pub mod stanza;
pub mod sticker_pack;

pub mod store;
pub mod time;
pub mod types;
pub mod upload;
pub mod usync;
pub mod webp;

pub mod version;
pub mod xml;
mod zip;
3 changes: 3 additions & 0 deletions wacore/src/send.rs
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,9 @@ pub fn media_type_from_message(msg: &wa::Message) -> Option<&'static str> {
if msg.sticker_message.is_some() {
return Some("sticker");
}
if msg.sticker_pack_message.is_some() {
return Some("sticker_pack");
}
if let Some(ref loc) = msg.location_message {
return if loc.is_live == Some(true) {
Some("livelocation")
Expand Down
Loading
Loading