Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
49 changes: 46 additions & 3 deletions src/upload.rs
Original file line number Diff line number Diff line change
Expand Up @@ -241,21 +241,64 @@ 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, Debug)]
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 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