Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
49 changes: 49 additions & 0 deletions .github/workflows/e2e.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
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
curl -k https://localhost:8080/ && break || sleep 1
done
Comment thread
coderabbitai[bot] marked this conversation as resolved.
- 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
4 changes: 2 additions & 2 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
run: cargo test --workspace --exclude e2e-tests --verbose -- --no-capture
26 changes: 16 additions & 10 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 1 addition & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ members = [
".",
"http_clients/ureq-client",
"storages/sqlite-storage",
"tests/e2e",
"transports/tokio-transport",
"wacore",
"wacore/appstate",
Expand Down Expand Up @@ -46,7 +47,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 }
Expand Down Expand Up @@ -87,7 +87,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"] }
Expand Down
44 changes: 41 additions & 3 deletions src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand All @@ -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();
Expand Down
1 change: 1 addition & 0 deletions src/handlers/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
72 changes: 72 additions & 0 deletions src/handlers/presence.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
//! Handler for incoming `<presence>` 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 `<presence>` 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<Client>, node: Arc<Node>, _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::<i64>().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
}
}
19 changes: 19 additions & 0 deletions tests/e2e/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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 }
Loading
Loading