diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml new file mode 100644 index 000000000..6f0a1cfbb --- /dev/null +++ b/.github/workflows/e2e.yml @@ -0,0 +1,55 @@ +name: E2E Tests + +on: + push: + branches: [main] + pull_request: + branches: [main] + +permissions: + contents: read + +env: + CARGO_TERM_COLOR: always + PROTOC_VERSION: '3.25.3' + +jobs: + e2e: + name: E2E Tests + runs-on: ubuntu-latest + services: + mock-server: + image: ghcr.io/whiskeysockets-devtools/bartender:latest + credentials: + username: ${{ github.actor }} + password: ${{ secrets.BARTENDER_GHCR_TOKEN }} + ports: + - 8080:8080 + options: --log-driver none + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@master + with: + toolchain: nightly-2026-01-30 + - name: Install protoc + uses: taiki-e/install-action@v2 + with: + tool: protoc@${{ env.PROTOC_VERSION }} + - name: Cache Rust dependencies + uses: Swatinem/rust-cache@v2 + - name: Wait for mock server + run: | + for i in $(seq 1 30); do + if curl -sk https://localhost:8080/ > /dev/null 2>&1; then + echo "Mock server is ready" + exit 0 + fi + sleep 1 + done + echo "Mock server failed to become ready" + exit 1 + - name: Run E2E tests + env: + MOCK_SERVER_URL: "wss://127.0.0.1:8080/ws/chat" + RUST_LOG: info + run: cargo test -p e2e-tests --verbose -- --no-capture diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 65df07ec0..a4fae5025 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -57,6 +57,6 @@ jobs: - name: Cache Rust dependencies uses: Swatinem/rust-cache@v2 - name: Build project - run: cargo build --all-targets --verbose + run: cargo build --workspace --exclude e2e-tests --all-targets --verbose - name: Run tests - run: cargo test --all --verbose -- --no-capture \ No newline at end of file + run: cargo test --workspace --exclude e2e-tests --verbose -- --no-capture \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index fddcb14bd..cd2d73cf7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -540,6 +540,22 @@ dependencies = [ "syn", ] +[[package]] +name = "e2e-tests" +version = "0.0.0" +dependencies = [ + "anyhow", + "env_logger", + "log", + "tokio", + "uuid", + "wacore", + "whatsapp-rust", + "whatsapp-rust-sqlite-storage", + "whatsapp-rust-tokio-transport", + "whatsapp-rust-ureq-http-client", +] + [[package]] name = "either" version = "1.15.0" @@ -1585,12 +1601,6 @@ dependencies = [ "getrandom 0.3.4", ] -[[package]] -name = "rand_core" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c8d0fd677905edcbeedbf2edb6494d676f0e98d54d5cf9bda0b061cb8fb8aba" - [[package]] name = "redox_syscall" version = "0.5.18" @@ -2299,7 +2309,6 @@ dependencies = [ "prost", "protobuf", "rand", - "rand_core 0.10.0", "serde", "serde-big-array", "serde_json", @@ -2396,12 +2405,10 @@ dependencies = [ "aes-gcm", "anyhow", "bytes", - "hex", "hkdf", "log", "prost", "rand", - "rand_core 0.10.0", "sha2", "thiserror 2.0.18", "wacore-binary", @@ -2547,7 +2554,6 @@ dependencies = [ "moka", "prost", "rand", - "rand_core 0.10.0", "scopeguard", "serde", "serde_json", diff --git a/Cargo.toml b/Cargo.toml index b489c210c..422a87a9c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,6 +11,20 @@ description = "Rust client for WhatsApp Web" [workspace] members = [ + ".", + "http_clients/ureq-client", + "storages/sqlite-storage", + "tests/e2e", + "transports/tokio-transport", + "wacore", + "wacore/appstate", + "wacore/binary", + "wacore/derive", + "wacore/libsignal", + "wacore/noise", + "waproto", +] +default-members = [ ".", "http_clients/ureq-client", "storages/sqlite-storage", @@ -46,7 +60,6 @@ log = "0.4" prost = { version = "0.14.1", default-features = false, features = ["std"] } prost-build = { version = "0.14.1", default-features = false } rand = "0.9" -rand_core = "0.10" serde = { version = "1.0", features = ["derive"] } serde-big-array = "0.5" serde_json = { version = "1.0", default-features = false } @@ -87,7 +100,6 @@ log = { workspace = true } moka = { version = "0.12.12", features = ["future"] } prost = { workspace = true } rand = { workspace = true } -rand_core = { workspace = true } scopeguard = "1.2" serde = { workspace = true } serde_json = { workspace = true, features = ["std"] } diff --git a/src/client.rs b/src/client.rs index 4642f3667..d3b84ad89 100644 --- a/src/client.rs +++ b/src/client.rs @@ -456,7 +456,7 @@ impl Client { // Register unimplemented handlers router.register(Arc::new(UnimplementedHandler::for_call())); - router.register(Arc::new(UnimplementedHandler::for_presence())); + router.register(Arc::new(crate::handlers::presence::PresenceHandler)); router } @@ -483,9 +483,47 @@ impl Client { &self, stanza: wacore::iq::chatstate::ChatstateStanza, ) { - let event = ChatStateEvent::from_stanza(stanza); + use wacore::iq::chatstate::{ChatstateSource, ReceivedChatState}; + use wacore::types::events::ChatPresenceUpdate; + use wacore::types::message::MessageSource; + use wacore::types::presence::{ChatPresence, ChatPresenceMedia}; + + // Dispatch via event bus + let (chat, sender, is_group) = match &stanza.source { + ChatstateSource::User { from } => (from.clone(), from.clone(), false), + ChatstateSource::Group { from, participant } => { + (from.clone(), participant.clone(), true) + } + }; - // Invoke handlers asynchronously + let (state, media) = match stanza.state { + ReceivedChatState::Typing => (ChatPresence::Composing, ChatPresenceMedia::Text), + ReceivedChatState::RecordingAudio => { + (ChatPresence::Composing, ChatPresenceMedia::Audio) + } + ReceivedChatState::Idle => (ChatPresence::Paused, ChatPresenceMedia::Text), + }; + + self.core + .event_bus + .dispatch(&Event::ChatPresence(ChatPresenceUpdate { + source: MessageSource { + chat, + sender, + is_from_me: false, + is_group, + addressing_mode: None, + sender_alt: None, + recipient_alt: None, + broadcast_list_owner: None, + recipient: None, + }, + state, + media, + })); + + // Invoke legacy callback handlers + let event = ChatStateEvent::from_stanza(stanza); let handlers = self.chatstate_handlers.read().await.clone(); for handler in handlers { let event_clone = event.clone(); diff --git a/src/handlers/mod.rs b/src/handlers/mod.rs index 5dfc6e120..4756c50f1 100644 --- a/src/handlers/mod.rs +++ b/src/handlers/mod.rs @@ -4,6 +4,7 @@ pub mod ib; pub mod iq; pub mod message; pub mod notification; +pub mod presence; pub mod receipt; pub mod router; pub mod traits; diff --git a/src/handlers/presence.rs b/src/handlers/presence.rs new file mode 100644 index 000000000..37149fca9 --- /dev/null +++ b/src/handlers/presence.rs @@ -0,0 +1,72 @@ +//! Handler for incoming `` stanzas. + +use super::traits::StanzaHandler; +use crate::client::Client; +use async_trait::async_trait; +use log::debug; +use std::sync::Arc; +use wacore::types::events::{Event, PresenceUpdate}; +use wacore_binary::node::Node; + +/// Handler for `` stanzas. +/// +/// Parses incoming presence updates and dispatches `Event::Presence` via the event bus. +#[derive(Default)] +pub struct PresenceHandler; + +#[async_trait] +impl StanzaHandler for PresenceHandler { + fn tag(&self) -> &'static str { + "presence" + } + + async fn handle(&self, client: Arc, node: Arc, _cancelled: &mut bool) -> bool { + let from = match node.attrs.get("from").map(|v| v.to_string()) { + Some(f) => f, + None => { + debug!(target: "PresenceHandler", "Presence stanza missing 'from' attribute"); + return true; + } + }; + + let from_jid = match from.parse() { + Ok(jid) => jid, + Err(e) => { + debug!(target: "PresenceHandler", "Failed to parse presence 'from' JID: {}", e); + return true; + } + }; + + let presence_type = node + .attrs + .get("type") + .map(|v| v.to_string()) + .unwrap_or_default(); + + let unavailable = presence_type == "unavailable"; + + // Parse last_seen from 'last' attribute if present + let last_seen = node + .attrs + .get("last") + .and_then(|v| v.to_string().parse::().ok()) + .and_then(|ts| chrono::DateTime::from_timestamp(ts, 0)); + + debug!( + target: "PresenceHandler", + "Received presence from {}: type={}, unavailable={}", + from, presence_type, unavailable + ); + + client + .core + .event_bus + .dispatch(&Event::Presence(PresenceUpdate { + from: from_jid, + unavailable, + last_seen, + })); + + true + } +} diff --git a/tests/e2e/Cargo.toml b/tests/e2e/Cargo.toml new file mode 100644 index 000000000..5b6087f7b --- /dev/null +++ b/tests/e2e/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "e2e-tests" +version = "0.0.0" +edition = "2024" +publish = false + +[dependencies] +anyhow = { workspace = true } +tokio = { workspace = true, features = ["macros", "rt-multi-thread", "sync", "time"] } +uuid = { version = "1.0", features = ["v4"] } +wacore = { path = "../../wacore" } +whatsapp-rust = { path = "../..", features = ["danger-skip-tls-verify"] } +whatsapp-rust-sqlite-storage = { path = "../../storages/sqlite-storage" } +whatsapp-rust-tokio-transport = { path = "../../transports/tokio-transport", features = ["danger-skip-tls-verify"] } +whatsapp-rust-ureq-http-client = { path = "../../http_clients/ureq-client" } + +[dev-dependencies] +env_logger = { version = "0.11", default-features = false } +log = { workspace = true } diff --git a/tests/e2e/src/lib.rs b/tests/e2e/src/lib.rs new file mode 100644 index 000000000..fee41ca96 --- /dev/null +++ b/tests/e2e/src/lib.rs @@ -0,0 +1,154 @@ +use std::sync::Arc; + +use wacore::types::events::{Event, EventHandler}; +use whatsapp_rust::bot::Bot; +use whatsapp_rust::store::traits::Backend; +use whatsapp_rust_sqlite_storage::SqliteStore; +use whatsapp_rust_tokio_transport::TokioWebSocketTransportFactory; +use whatsapp_rust_ureq_http_client::UreqHttpClient; + +/// Creates a SqliteStore with a unique in-memory database for test isolation. +pub async fn create_test_store(prefix: &str) -> anyhow::Result { + let db = format!( + "file:{}_{}?mode=memory&cache=shared", + prefix, + uuid::Uuid::new_v4() + ); + Ok(SqliteStore::new(&db).await?) +} + +/// Returns the mock server WebSocket URL from env, or the default. +pub fn mock_server_url() -> String { + std::env::var("MOCK_SERVER_URL").unwrap_or_else(|_| "wss://127.0.0.1:8080/ws/chat".to_string()) +} + +/// Event handler that sends events to a tokio broadcast channel for test assertions. +pub struct ChannelEventHandler { + tx: tokio::sync::broadcast::Sender, +} + +impl ChannelEventHandler { + pub fn new() -> (Arc, tokio::sync::broadcast::Receiver) { + let (tx, rx) = tokio::sync::broadcast::channel(100); + (Arc::new(Self { tx }), rx) + } +} + +impl EventHandler for ChannelEventHandler { + fn handle_event(&self, event: &Event) { + let _ = self.tx.send(event.clone()); + } +} + +/// A connected client ready for testing, with its event receiver and run handle. +pub struct TestClient { + pub client: Arc, + pub event_rx: tokio::sync::broadcast::Receiver, + pub run_handle: tokio::task::JoinHandle<()>, +} + +impl TestClient { + /// Create a client, connect to the mock server, and wait for PairSuccess + Connected. + /// Returns the connected TestClient with its JID available via `client.get_pn()`. + pub async fn connect(prefix: &str) -> anyhow::Result { + let store = create_test_store(prefix).await?; + let backend = Arc::new(store) as Arc; + let transport_factory = TokioWebSocketTransportFactory::new().with_url(mock_server_url()); + let (event_handler, mut event_rx) = ChannelEventHandler::new(); + + let mut bot = Bot::builder() + .with_backend(backend) + .with_transport_factory(transport_factory) + .with_http_client(UreqHttpClient::new()) + .build() + .await?; + + let client = bot.client(); + client.register_handler(event_handler); + let run_handle = bot.run().await?; + + // Wait for PairSuccess + Connected + let timeout = tokio::time::Duration::from_secs(30); + let mut got_pair = false; + let mut got_connected = false; + + let wait_result = tokio::time::timeout(timeout, async { + loop { + match event_rx.recv().await { + Ok(Event::PairSuccess(_)) => { + got_pair = true; + if got_connected { + break; + } + } + Ok(Event::Connected(_)) => { + got_connected = true; + if got_pair { + break; + } + } + Ok(_) => {} + Err(e) => { + return Err(anyhow::anyhow!("Event channel error during connect: {e}")); + } + } + } + Ok(()) + }) + .await; + + match wait_result { + Err(_) => { + client.disconnect().await; + run_handle.abort(); + return Err(anyhow::anyhow!( + "Timed out waiting for PairSuccess + Connected" + )); + } + Ok(Err(e)) => { + client.disconnect().await; + run_handle.abort(); + return Err(e); + } + Ok(Ok(())) => {} + } + + assert!(got_pair, "Should have received PairSuccess"); + assert!(got_connected, "Should have received Connected"); + + Ok(Self { + client, + event_rx, + run_handle, + }) + } + + /// Wait for an event matching the predicate, with a timeout in seconds. + pub async fn wait_for_event( + &mut self, + timeout_secs: u64, + mut predicate: F, + ) -> anyhow::Result + where + F: FnMut(&Event) -> bool, + { + let timeout = tokio::time::Duration::from_secs(timeout_secs); + tokio::time::timeout(timeout, async { + loop { + match self.event_rx.recv().await { + Ok(event) if predicate(&event) => return Ok(event), + Ok(_) => continue, + Err(e) => return Err(anyhow::anyhow!("Event channel error: {e}")), + } + } + }) + .await + .map_err(|_| anyhow::anyhow!("Timed out waiting for event"))? + } + + /// Disconnect and abort the run handle. + pub async fn disconnect(self) { + self.client.disconnect().await; + self.run_handle.abort(); + } +} diff --git a/tests/e2e/tests/connection.rs b/tests/e2e/tests/connection.rs new file mode 100644 index 000000000..b7d56a194 --- /dev/null +++ b/tests/e2e/tests/connection.rs @@ -0,0 +1,124 @@ +use std::sync::Arc; + +use e2e_tests::{ChannelEventHandler, TestClient, create_test_store, mock_server_url}; +use log::info; +use wacore::types::events::Event; +use whatsapp_rust::bot::Bot; +use whatsapp_rust::store::traits::Backend; +use whatsapp_rust_tokio_transport::TokioWebSocketTransportFactory; +use whatsapp_rust_ureq_http_client::UreqHttpClient; + +#[tokio::test] +async fn test_connect_and_pair() -> anyhow::Result<()> { + let _ = env_logger::builder().is_test(true).try_init(); + + let ws_url = mock_server_url(); + info!("Connecting to mock server at {}", ws_url); + + // Create storage + let store = create_test_store("e2e_connect").await?; + let backend = Arc::new(store) as Arc; + + // Create transport pointing to mock server (TLS verification is skipped via feature flag) + let transport_factory = TokioWebSocketTransportFactory::new().with_url(&ws_url); + + // Build bot with event capture + let (event_handler, mut event_rx) = ChannelEventHandler::new(); + + let mut bot = Bot::builder() + .with_backend(backend) + .with_transport_factory(transport_factory) + .with_http_client(UreqHttpClient::new()) + .build() + .await?; + + // Register event handler before running + let client = bot.client(); + client.register_handler(event_handler); + + // Run bot in background + let run_handle = bot.run().await?; + + // Wait for PairSuccess and Connected events (mock server auto-pairs after ~2s) + let timeout = tokio::time::Duration::from_secs(30); + let mut got_pair_success = false; + let mut got_connected = false; + + let result = tokio::time::timeout(timeout, async { + loop { + match event_rx.recv().await { + Ok(Event::PairSuccess(ps)) => { + info!("Received PairSuccess: {:?}", ps); + got_pair_success = true; + if got_connected { + break; + } + } + Ok(Event::Connected(_)) => { + info!("Received Connected event"); + got_connected = true; + if got_pair_success { + break; + } + } + Ok(event) => { + info!("Received event: {:?}", event); + } + Err(e) => { + panic!("Event channel error: {}", e); + } + } + } + }) + .await; + + assert!( + result.is_ok(), + "Timed out waiting for PairSuccess + Connected events" + ); + assert!(got_pair_success, "Should have received PairSuccess"); + assert!(got_connected, "Should have received Connected"); + + // Verify the client is logged in + assert!( + client.is_logged_in(), + "Client should be logged in after pairing" + ); + + // Cleanup + client.disconnect().await; + run_handle.abort(); + + Ok(()) +} + +#[tokio::test] +async fn test_reconnect_after_disconnect() -> anyhow::Result<()> { + let _ = env_logger::builder().is_test(true).try_init(); + + // Connect and pair + let tc = TestClient::connect("e2e_reconnect").await?; + info!("First connection established"); + + assert!(tc.client.is_logged_in()); + + // Keep client and store, disconnect + let client = tc.client.clone(); + client.disconnect().await; + tc.run_handle.abort(); + + // Give a moment for cleanup + tokio::time::sleep(tokio::time::Duration::from_millis(500)).await; + + // Reconnect using the same persisted store — should not need re-pairing. + // We need a new Bot with the same backend. Since TestClient consumed the store, + // we verify the client was logged in before disconnecting. + // For a full reconnect test, we'd need to persist the store and rebuild. + // For now, verify the disconnect was clean. + assert!( + !client.is_logged_in(), + "Client should not be logged in after disconnect" + ); + + Ok(()) +} diff --git a/tests/e2e/tests/messaging.rs b/tests/e2e/tests/messaging.rs new file mode 100644 index 000000000..6a96ccce4 --- /dev/null +++ b/tests/e2e/tests/messaging.rs @@ -0,0 +1,197 @@ +use e2e_tests::TestClient; +use log::info; +use wacore::types::events::Event; +use whatsapp_rust::waproto::whatsapp as wa; + +#[tokio::test] +async fn test_send_text_message() -> anyhow::Result<()> { + let _ = env_logger::builder().is_test(true).try_init(); + + // Connect two clients + let client_a = TestClient::connect("e2e_msg_a").await?; + let mut client_b = TestClient::connect("e2e_msg_b").await?; + + let jid_b = client_b + .client + .get_pn() + .await + .expect("Client B should have a JID") + .to_non_ad(); + + info!("Client B JID: {jid_b}"); + + // Client A sends a text message to Client B + let text = "Hello from client A!"; + let message = wa::Message { + conversation: Some(text.to_string()), + ..Default::default() + }; + + let msg_id = client_a.client.send_message(jid_b.clone(), message).await?; + info!("Client A sent message with id: {msg_id}"); + + // Client B should receive the message + let event = client_b + .wait_for_event(30, |e| matches!(e, Event::Message(_, _))) + .await?; + + if let Event::Message(msg, info) = event { + info!( + "Client B received message from {:?}: {:?}", + info.source, msg + ); + assert_eq!( + msg.conversation.as_deref(), + Some(text), + "Received message text should match sent text" + ); + } else { + panic!("Expected Message event"); + } + + // Cleanup + client_a.disconnect().await; + client_b.disconnect().await; + + Ok(()) +} + +#[tokio::test] +async fn test_send_text_message_bidirectional() -> anyhow::Result<()> { + let _ = env_logger::builder().is_test(true).try_init(); + + let mut client_a = TestClient::connect("e2e_bidir_a").await?; + let mut client_b = TestClient::connect("e2e_bidir_b").await?; + + let jid_a = client_a + .client + .get_pn() + .await + .expect("Client A should have a JID") + .to_non_ad(); + let jid_b = client_b + .client + .get_pn() + .await + .expect("Client B should have a JID") + .to_non_ad(); + + info!("Client A JID: {jid_a}, Client B JID: {jid_b}"); + + // A -> B + let text_a = "Hello B, this is A!"; + client_a + .client + .send_message( + jid_b.clone(), + wa::Message { + conversation: Some(text_a.to_string()), + ..Default::default() + }, + ) + .await?; + + let event = client_b + .wait_for_event(30, |e| matches!(e, Event::Message(_, _))) + .await?; + if let Event::Message(msg, _) = event { + assert_eq!(msg.conversation.as_deref(), Some(text_a)); + } else { + panic!("Expected Message event, got: {:?}", event); + } + + // B -> A + let text_b = "Hello A, this is B!"; + client_b + .client + .send_message( + jid_a.clone(), + wa::Message { + conversation: Some(text_b.to_string()), + ..Default::default() + }, + ) + .await?; + + let event = client_a + .wait_for_event(30, |e| matches!(e, Event::Message(_, _))) + .await?; + if let Event::Message(msg, _) = event { + assert_eq!(msg.conversation.as_deref(), Some(text_b)); + } else { + panic!("Expected Message event, got: {:?}", event); + } + + client_a.disconnect().await; + client_b.disconnect().await; + + Ok(()) +} + +#[tokio::test] +async fn test_message_revoke() -> anyhow::Result<()> { + let _ = env_logger::builder().is_test(true).try_init(); + + let client_a = TestClient::connect("e2e_revoke_a").await?; + let mut client_b = TestClient::connect("e2e_revoke_b").await?; + + let jid_b = client_b + .client + .get_pn() + .await + .expect("Client B should have a JID") + .to_non_ad(); + + // Send a message + let msg_id = client_a + .client + .send_message( + jid_b.clone(), + wa::Message { + conversation: Some("This will be revoked".to_string()), + ..Default::default() + }, + ) + .await?; + + // Wait for B to receive it + client_b + .wait_for_event(30, |e| matches!(e, Event::Message(_, _))) + .await?; + + // Revoke the message + client_a + .client + .revoke_message( + jid_b, + msg_id.clone(), + whatsapp_rust::send::RevokeType::Sender, + ) + .await?; + info!("Client A revoked message {msg_id}"); + + // Client B should receive the revoke as a protocol message + let event = client_b + .wait_for_event(30, |e| { + if let Event::Message(msg, _) = e { + msg.protocol_message.is_some() + } else { + false + } + }) + .await?; + + if let Event::Message(msg, _) = event { + let proto = msg.protocol_message.as_ref().unwrap(); + assert_eq!( + proto.r#type(), + wa::message::protocol_message::Type::Revoke, + "Should be a revoke protocol message" + ); + } + + client_a.disconnect().await; + client_b.disconnect().await; + + Ok(()) +} diff --git a/tests/e2e/tests/presence.rs b/tests/e2e/tests/presence.rs new file mode 100644 index 000000000..936e75e4e --- /dev/null +++ b/tests/e2e/tests/presence.rs @@ -0,0 +1,77 @@ +use e2e_tests::TestClient; +use log::info; +use wacore::types::events::Event; + +#[tokio::test] +async fn test_typing_indicator() -> anyhow::Result<()> { + let _ = env_logger::builder().is_test(true).try_init(); + + let client_a = TestClient::connect("e2e_typing_a").await?; + let mut client_b = TestClient::connect("e2e_typing_b").await?; + + let jid_b = client_b + .client + .get_pn() + .await + .expect("Client B should have a JID") + .to_non_ad(); + + info!("Client A sending typing indicator to {jid_b}"); + + // Client A starts typing to Client B + client_a.client.chatstate().send_composing(&jid_b).await?; + + // Client B should receive a ChatPresence event + let event = client_b + .wait_for_event(15, |e| matches!(e, Event::ChatPresence(_))) + .await?; + + if let Event::ChatPresence(presence) = event { + info!("Client B received chat presence: {:?}", presence); + } else { + panic!("Expected ChatPresence event"); + } + + client_a.disconnect().await; + client_b.disconnect().await; + + Ok(()) +} + +#[tokio::test] +async fn test_presence_available() -> anyhow::Result<()> { + let _ = env_logger::builder().is_test(true).try_init(); + + let client_a = TestClient::connect("e2e_presence_a").await?; + let mut client_b = TestClient::connect("e2e_presence_b").await?; + + let jid_a = client_a + .client + .get_pn() + .await + .expect("Client A should have a JID") + .to_non_ad(); + + // Client B subscribes to Client A's presence + client_b.client.presence().subscribe(&jid_a).await?; + + // Client A sets available + client_a.client.presence().set_available().await?; + info!("Client A set presence to available"); + + // Client B should receive a Presence event + let event = client_b + .wait_for_event(15, |e| matches!(e, Event::Presence(_))) + .await?; + + if let Event::Presence(presence) = event { + info!("Client B received presence update: {:?}", presence); + } else { + panic!("Expected Presence event"); + } + + client_a.disconnect().await; + client_b.disconnect().await; + + Ok(()) +} diff --git a/wacore/Cargo.toml b/wacore/Cargo.toml index edb8a8408..ac5162d18 100644 --- a/wacore/Cargo.toml +++ b/wacore/Cargo.toml @@ -32,7 +32,6 @@ pbkdf2 = { version = "0.12", default-features = false, features = ["hmac"] } prost = { workspace = true } protobuf = { version = "3", default-features = false } rand = { workspace = true } -rand_core = { workspace = true } serde = { workspace = true } serde-big-array = { workspace = true } serde_json = { workspace = true, features = ["std"] } diff --git a/wacore/binary/Cargo.toml b/wacore/binary/Cargo.toml index 2b6b1af08..9800a5057 100644 --- a/wacore/binary/Cargo.toml +++ b/wacore/binary/Cargo.toml @@ -7,6 +7,9 @@ license = "MIT" repository = "https://github.com/jlucaso1/whatsapp-rust" description = "Binary data and constants for WhatsApp protocol" +[package.metadata.cargo-shear] +ignored = ["phf"] + [lib] crate-type = ["cdylib", "rlib"] diff --git a/wacore/noise/Cargo.toml b/wacore/noise/Cargo.toml index 764c44019..df962f44b 100644 --- a/wacore/noise/Cargo.toml +++ b/wacore/noise/Cargo.toml @@ -18,7 +18,6 @@ hkdf = { workspace = true } log = { workspace = true } prost = { workspace = true } rand = { workspace = true } -rand_core = { workspace = true } sha2 = { workspace = true } thiserror = { workspace = true } wacore-binary = { workspace = true } @@ -26,4 +25,3 @@ wacore-libsignal = { workspace = true } waproto = { workspace = true } [dev-dependencies] -hex = { workspace = true } diff --git a/wacore/src/types/newsletter.rs b/wacore/src/types/newsletter.rs deleted file mode 100644 index 5ced5cb66..000000000 --- a/wacore/src/types/newsletter.rs +++ /dev/null @@ -1,117 +0,0 @@ -use chrono::{DateTime, Utc}; -use serde::{Deserialize, Serialize}; -use std::collections::HashMap; -use wacore_binary::jid::{Jid, MessageId, MessageServerId}; -use waproto::whatsapp as wa; - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[serde(rename_all = "lowercase")] -pub enum NewsletterVerificationState { - Verified, - Unverified, -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[serde(rename_all = "lowercase")] -pub enum NewsletterPrivacy { - Private, - Public, -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[serde(rename_all = "snake_case")] -pub enum NewsletterReactionsMode { - All, - Basic, - None, - Blocklist, -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[serde(rename_all = "lowercase")] -pub enum NewsletterState { - Active, - Suspended, - Geosuspended, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct WrappedNewsletterState { - pub r#type: NewsletterState, -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[serde(rename_all = "lowercase")] -pub enum NewsletterMuteState { - On, - Off, -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[serde(rename_all = "lowercase")] -pub enum NewsletterRole { - Subscriber, - Guest, - Admin, - Owner, -} - -#[derive(Debug, Clone, Serialize)] -pub struct NewsletterMetadata { - pub id: Jid, - pub state: WrappedNewsletterState, - pub thread_metadata: NewsletterThreadMetadata, - pub viewer_metadata: Option, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct NewsletterViewerMetadata { - pub mute: NewsletterMuteState, - pub role: NewsletterRole, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct NewsletterReactionSettings { - pub value: NewsletterReactionsMode, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct NewsletterSettings { - pub reaction_codes: NewsletterReactionSettings, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct NewsletterThreadMetadata { - #[serde(with = "chrono::serde::ts_seconds")] - pub creation_time: DateTime, - #[serde(rename = "invite")] - pub invite_code: String, - pub name: NewsletterText, - pub description: NewsletterText, - #[serde(rename = "subscribers_count")] - pub subscriber_count: i32, - #[serde(rename = "verification")] - pub verification_state: NewsletterVerificationState, - pub picture_url: Option, - pub preview_url: Option, - pub settings: NewsletterSettings, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct NewsletterText { - pub text: String, - pub id: String, - #[serde(with = "chrono::serde::ts_microseconds")] - pub update_time: DateTime, -} - -#[derive(Debug, Clone, Serialize)] -pub struct NewsletterMessage { - pub message_server_id: MessageServerId, - pub message_id: MessageId, - pub r#type: String, - pub timestamp: DateTime, - pub views_count: i32, - pub reaction_counts: HashMap, - pub message: Option, -}