diff --git a/examples/benchmark.rs b/examples/benchmark.rs index bbcf0a6b5..5edc0c8f8 100644 --- a/examples/benchmark.rs +++ b/examples/benchmark.rs @@ -47,21 +47,15 @@ fn main() { let mut bot = builder .on_event(move |event, client| async move { - match event { + match &*event { Event::Message(msg, info) => { - let ctx = MessageContext { - message: msg, - info, - client, - }; - - if let Some(text) = ctx.message.text_content() + if let Some(text) = msg.text_content() && text == "ping" { + let ctx = MessageContext::from_parts(msg, info, client); info!("Received text ping, sending pong..."); let pong_text = format!("pong {}", ctx.info.id); - let reply_message = wa::Message { conversation: Some(pong_text), ..Default::default() diff --git a/src/bot.rs b/src/bot.rs index b45c7925e..75cff58a1 100644 --- a/src/bot.rs +++ b/src/bot.rs @@ -36,6 +36,19 @@ pub struct MessageContext { } impl MessageContext { + pub fn from_parts(message: &wa::Message, info: &MessageInfo, client: Arc) -> Self { + Self { + message: Box::new(message.clone()), + info: info.clone(), + client, + } + } + + pub fn from_event(event: &Event, client: Arc) -> Option { + let (msg, info) = event.as_message()?; + Some(Self::from_parts(msg, info, client)) + } + pub async fn send_message( &self, message: wa::Message, @@ -45,17 +58,7 @@ impl MessageContext { .await } - /// Build a quote context for this message. - /// - /// Handles: - /// - Correct stanza_id/participant (newsletters + group status) - /// - Stripping nested mentions to avoid accidental tags - /// - Preserving bot quote chains (matches WhatsApp Web) - /// - /// Use this when you need manual control but want correct quoting behavior. pub fn build_quote_context(&self) -> wa::ContextInfo { - // Use the standalone function from wacore with full message info - // This handles newsletter/group status participant resolution wacore::proto_helpers::build_quote_context_with_info( &self.info.id, &self.info.source.sender, @@ -91,7 +94,7 @@ impl MessageContext { } type EventHandlerCallback = - Arc) -> Pin + Send>> + Send + Sync>; + Arc, Arc) -> Pin + Send>> + Send + Sync>; struct BotEventHandler { client: Arc, @@ -99,16 +102,15 @@ struct BotEventHandler { } impl EventHandler for BotEventHandler { - fn handle_event(&self, event: &Event) { + fn handle_event(&self, event: Arc) { if let Some(handler) = &self.event_handler { let handler_clone = handler.clone(); - let event_clone = event.clone(); let client_clone = self.client.clone(); self.client .runtime .spawn(Box::pin(async move { - handler_clone(event_clone, client_clone).await; + handler_clone(event, client_clone).await; })) .detach(); } @@ -425,7 +427,7 @@ impl BotBuilder { impl BotBuilder { pub fn on_event(mut self, handler: F) -> Self where - F: Fn(Event, Arc) -> Fut + Send + Sync + 'static, + F: Fn(Arc, Arc) -> Fut + Send + Sync + 'static, Fut: Future + Send + 'static, { self.event_handler = Some(Arc::new(move |event, client| { @@ -549,7 +551,7 @@ impl BotBuilder { /// platform_display: "Chrome (Linux)".to_string(), /// }) /// .on_event(|event, client| async move { - /// match event { + /// match &*event { /// Event::PairingCode { code, timeout } => { /// println!("Enter this code on your phone: {}", code); /// } diff --git a/src/client.rs b/src/client.rs index 9d150f645..bd3cd05bb 100644 --- a/src/client.rs +++ b/src/client.rs @@ -544,7 +544,7 @@ impl Client { self.is_ready.store(true, Ordering::Relaxed); self.core .event_bus - .dispatch(&Event::Connected(crate::types::events::Connected)); + .dispatch(Event::Connected(crate::types::events::Connected)); self.connected_notifier.notify(usize::MAX); } @@ -939,7 +939,7 @@ impl Client { self.core .event_bus - .dispatch(&Event::ChatPresence(ChatPresenceUpdate { + .dispatch(Event::ChatPresence(ChatPresenceUpdate { source: MessageSource { chat, sender, @@ -1014,7 +1014,7 @@ impl Client { if unexpected_disconnect { self.core .event_bus - .dispatch(&Event::Disconnected(crate::types::events::Disconnected)); + .dispatch(Event::Disconnected(crate::types::events::Disconnected)); } } @@ -1147,7 +1147,7 @@ impl Client { self.core .event_bus - .dispatch(&Event::LoggedOut(crate::types::events::LoggedOut { + .dispatch(Event::LoggedOut(crate::types::events::LoggedOut { on_connect: false, reason: ConnectFailureReason::LoggedOut, })); @@ -1640,7 +1640,7 @@ impl Client { if self.raw_node_forwarding.load(Ordering::Relaxed) { self.core .event_bus - .dispatch(&Event::RawNode(Arc::clone(&node))); + .dispatch(Event::RawNode(Arc::clone(&node))); } if nr.tag.as_ref() == "xmlstreamend" { @@ -2980,7 +2980,7 @@ impl Client { self.persistence_manager .process_command(DeviceCommand::SetPushName(new_name.clone())) .await; - bus.dispatch(&Event::SelfPushNameUpdated( + bus.dispatch(Event::SelfPushNameUpdated( crate::types::events::SelfPushNameUpdated { from_server: true, old_name: old.clone(), @@ -3037,7 +3037,7 @@ impl Client { reason: ConnectFailureReason::LoggedOut, }) }; - self.core.event_bus.dispatch(&event); + self.core.event_bus.dispatch(event); should_disconnect = true; } else { match code { @@ -3052,7 +3052,7 @@ impl Client { info!("Got 516 stream error (device removed). Logging out."); self.expected_disconnect.store(true, Ordering::Relaxed); self.enable_auto_reconnect.store(false, Ordering::Relaxed); - self.core.event_bus.dispatch(&Event::LoggedOut( + self.core.event_bus.dispatch(Event::LoggedOut( crate::types::events::LoggedOut { on_connect: false, reason: ConnectFailureReason::LoggedOut, @@ -3064,7 +3064,7 @@ impl Client { info!("Got 401 stream error (unauthorized). Logging out."); self.expected_disconnect.store(true, Ordering::Relaxed); self.enable_auto_reconnect.store(false, Ordering::Relaxed); - self.core.event_bus.dispatch(&Event::LoggedOut( + self.core.event_bus.dispatch(Event::LoggedOut( crate::types::events::LoggedOut { on_connect: false, reason: ConnectFailureReason::LoggedOut, @@ -3078,7 +3078,7 @@ impl Client { self.enable_auto_reconnect.store(false, Ordering::Relaxed); self.core .event_bus - .dispatch(&Event::StreamReplaced(crate::types::events::StreamReplaced)); + .dispatch(Event::StreamReplaced(crate::types::events::StreamReplaced)); should_disconnect = true; } "429" => { @@ -3093,7 +3093,7 @@ impl Client { _ => { error!("Unknown stream error: {}", DisplayableNodeRef(node)); self.expected_disconnect.store(true, Ordering::Relaxed); - self.core.event_bus.dispatch(&Event::StreamError( + self.core.event_bus.dispatch(Event::StreamError( crate::types::events::StreamError { code: code.to_string(), raw: Some(node.to_owned()), @@ -3137,7 +3137,7 @@ impl Client { info!("Got {reason:?} connect failure, logging out."); self.core .event_bus - .dispatch(&wacore::types::events::Event::LoggedOut( + .dispatch(wacore::types::events::Event::LoggedOut( crate::types::events::LoggedOut { on_connect: true, reason, @@ -3152,20 +3152,20 @@ impl Client { "Temporary ban connect failure: {}", DisplayableNodeRef(node) ); - self.core.event_bus.dispatch(&Event::TemporaryBan( - crate::types::events::TemporaryBan { + self.core + .event_bus + .dispatch(Event::TemporaryBan(crate::types::events::TemporaryBan { code: crate::types::events::TempBanReason::from(ban_code), expire: expire_duration, - }, - )); + })); } else if let ConnectFailureReason::ClientOutdated = reason { error!("Client is outdated and was rejected by server."); self.core .event_bus - .dispatch(&Event::ClientOutdated(crate::types::events::ClientOutdated)); + .dispatch(Event::ClientOutdated(crate::types::events::ClientOutdated)); } else { warn!("Unknown connect failure: {}", DisplayableNodeRef(node)); - self.core.event_bus.dispatch(&Event::ConnectFailure( + self.core.event_bus.dispatch(Event::ConnectFailure( crate::types::events::ConnectFailure { reason, message: attrs @@ -3486,7 +3486,7 @@ impl Client { .process_command(DeviceCommand::SetPushName(new_name.clone())) .await; - self.core.event_bus.dispatch(&Event::SelfPushNameUpdated( + self.core.event_bus.dispatch(Event::SelfPushNameUpdated( crate::types::events::SelfPushNameUpdated { from_server: true, old_name, diff --git a/src/client/sessions.rs b/src/client/sessions.rs index 7694d000a..184d25889 100644 --- a/src/client/sessions.rs +++ b/src/client/sessions.rs @@ -43,7 +43,7 @@ impl Client { self.core .event_bus - .dispatch(&Event::OfflineSyncCompleted(OfflineSyncCompleted { count })); + .dispatch(Event::OfflineSyncCompleted(OfflineSyncCompleted { count })); } } diff --git a/src/features/chat_actions.rs b/src/features/chat_actions.rs index 5dc76ff84..9f413fb28 100644 --- a/src/features/chat_actions.rs +++ b/src/features/chat_actions.rs @@ -112,7 +112,7 @@ pub(crate) fn dispatch_chat_mutation( if let Some(val) = &m.action_value && let Some(act) = &val.mute_action { - event_bus.dispatch(&Event::MuteUpdate(MuteUpdate { + event_bus.dispatch(Event::MuteUpdate(MuteUpdate { jid, timestamp: time, action: Box::new(*act), @@ -125,7 +125,7 @@ pub(crate) fn dispatch_chat_mutation( if let Some(val) = &m.action_value && let Some(act) = &val.pin_action { - event_bus.dispatch(&Event::PinUpdate(PinUpdate { + event_bus.dispatch(Event::PinUpdate(PinUpdate { jid, timestamp: time, action: Box::new(*act), @@ -138,7 +138,7 @@ pub(crate) fn dispatch_chat_mutation( if let Some(val) = &m.action_value && let Some(act) = &val.archive_chat_action { - event_bus.dispatch(&Event::ArchiveUpdate(ArchiveUpdate { + event_bus.dispatch(Event::ArchiveUpdate(ArchiveUpdate { jid, timestamp: time, action: Box::new(act.clone()), @@ -153,7 +153,7 @@ pub(crate) fn dispatch_chat_mutation( && let Some((message_id, from_me, participant_jid)) = parse_message_key_fields(kind, &m.index) { - event_bus.dispatch(&Event::StarUpdate(StarUpdate { + event_bus.dispatch(Event::StarUpdate(StarUpdate { chat_jid: jid, participant_jid, message_id, @@ -169,7 +169,7 @@ pub(crate) fn dispatch_chat_mutation( if let Some(val) = &m.action_value && let Some(act) = &val.contact_action { - event_bus.dispatch(&Event::ContactUpdate(ContactUpdate { + event_bus.dispatch(Event::ContactUpdate(ContactUpdate { jid, timestamp: time, action: Box::new(act.clone()), @@ -182,7 +182,7 @@ pub(crate) fn dispatch_chat_mutation( if let Some(val) = &m.action_value && let Some(act) = &val.mark_chat_as_read_action { - event_bus.dispatch(&Event::MarkChatAsReadUpdate(MarkChatAsReadUpdate { + event_bus.dispatch(Event::MarkChatAsReadUpdate(MarkChatAsReadUpdate { jid, timestamp: time, action: Box::new(act.clone()), @@ -197,7 +197,7 @@ pub(crate) fn dispatch_chat_mutation( { // delete_media is in index[2], not in the proto (which only has messageRange) let delete_media = m.index.get(2).is_none_or(|v| v != "0"); - event_bus.dispatch(&Event::DeleteChatUpdate(DeleteChatUpdate { + event_bus.dispatch(Event::DeleteChatUpdate(DeleteChatUpdate { jid, delete_media, timestamp: time, @@ -213,7 +213,7 @@ pub(crate) fn dispatch_chat_mutation( && let Some((message_id, from_me, participant_jid)) = parse_message_key_fields(kind, &m.index) { - event_bus.dispatch(&Event::DeleteMessageForMeUpdate(DeleteMessageForMeUpdate { + event_bus.dispatch(Event::DeleteMessageForMeUpdate(DeleteMessageForMeUpdate { chat_jid: jid, participant_jid, message_id, diff --git a/src/handlers/ib.rs b/src/handlers/ib.rs index 3b98bfa8c..1ee5291b0 100644 --- a/src/handlers/ib.rs +++ b/src/handlers/ib.rs @@ -148,7 +148,7 @@ async fn handle_ib_impl(client: Arc, node: &wacore_binary::NodeRef<'_>) client .core .event_bus - .dispatch(&Event::OfflineSyncPreview(OfflineSyncPreview { + .dispatch(Event::OfflineSyncPreview(OfflineSyncPreview { total, app_data_changes, messages, diff --git a/src/handlers/notification.rs b/src/handlers/notification.rs index 48e042cba..0ec75df55 100644 --- a/src/handlers/notification.rs +++ b/src/handlers/notification.rs @@ -252,7 +252,7 @@ async fn handle_notification_impl(client: &Arc, node: Arc) client .core .event_bus - .dispatch(&Event::Notification(Arc::clone(&node))); + .dispatch(Event::Notification(Arc::clone(&node))); } } } @@ -419,7 +419,7 @@ async fn handle_identity_change(client: &Arc, node: &NodeRef<'_>) { client.invalidate_device_cache(&from_jid.user).await; let session_jid = from_jid.clone(); - client.core.event_bus.dispatch(&Event::IdentityChange( + client.core.event_bus.dispatch(Event::IdentityChange( crate::types::events::IdentityChange { user: from_jid, lid_user: node.attrs().optional_jid("lid"), @@ -525,7 +525,7 @@ async fn handle_devices_notification(client: &Arc, node: &NodeRef<'_>) { key_index: op.key_index.clone(), contact_hash: op.contact_hash.clone(), }); - client.core.event_bus.dispatch(&event); + client.core.event_bus.dispatch(event); } /// Parsed device info from account_sync notification @@ -894,7 +894,7 @@ async fn handle_business_notification(client: &Arc, node: &NodeRef<'_>) _ => {} } - client.core.event_bus.dispatch(&event); + client.core.event_bus.dispatch(event); } /// Handle profile picture change notifications. @@ -991,7 +991,7 @@ fn handle_picture_notification(client: &Arc, node: &NodeRef<'_>) { removed, picture_id, }); - client.core.event_bus.dispatch(&event); + client.core.event_bus.dispatch(event); } /// Handle status/about text change notifications. @@ -1032,7 +1032,7 @@ fn handle_status_notification(client: &Arc, node: &NodeRef<'_>) { status: status_text, timestamp, }); - client.core.event_bus.dispatch(&event); + client.core.event_bus.dispatch(event); } else { debug!( target: "Client/Status", @@ -1126,7 +1126,7 @@ async fn handle_contacts_notification(client: &Arc, node: &NodeRef<'_>) client .core .event_bus - .dispatch(&Event::ContactUpdated(ContactUpdated { jid, timestamp })); + .dispatch(Event::ContactUpdated(ContactUpdated { jid, timestamp })); } "modify" => { // WA Web: old/new are PN JIDs, old_lid/new_lid are optional LID JIDs. @@ -1159,7 +1159,7 @@ async fn handle_contacts_notification(client: &Arc, node: &NodeRef<'_>) client .core .event_bus - .dispatch(&Event::ContactNumberChanged(ContactNumberChanged { + .dispatch(Event::ContactNumberChanged(ContactNumberChanged { old_jid, new_jid, old_lid, @@ -1181,7 +1181,7 @@ async fn handle_contacts_notification(client: &Arc, node: &NodeRef<'_>) client .core .event_bus - .dispatch(&Event::ContactSyncRequested(ContactSyncRequested { + .dispatch(Event::ContactSyncRequested(ContactSyncRequested { after, timestamp, })); @@ -1274,7 +1274,7 @@ async fn handle_group_notification(client: &Arc, node: Arc client .core .event_bus - .dispatch(&Event::GroupUpdate(GroupUpdate { + .dispatch(Event::GroupUpdate(GroupUpdate { group_jid: notification.group_jid.clone(), participant: notification.participant.clone(), participant_pn: notification.participant_pn.clone(), @@ -1288,7 +1288,7 @@ async fn handle_group_notification(client: &Arc, node: Arc client .core .event_bus - .dispatch(&Event::Notification(Arc::clone(&node))); + .dispatch(Event::Notification(Arc::clone(&node))); } /// Handle `` — live updates with reaction counts. @@ -1349,7 +1349,7 @@ fn handle_newsletter_notification(client: &Arc, node: Arc) client .core .event_bus - .dispatch(&Event::NewsletterLiveUpdate(NewsletterLiveUpdate { + .dispatch(Event::NewsletterLiveUpdate(NewsletterLiveUpdate { newsletter_jid, messages, })); @@ -1360,7 +1360,7 @@ fn handle_newsletter_notification(client: &Arc, node: Arc) client .core .event_bus - .dispatch(&Event::Notification(Arc::clone(&node))); + .dispatch(Event::Notification(Arc::clone(&node))); } /// Handle `` — a contact changed @@ -1413,7 +1413,7 @@ fn handle_disappearing_mode_notification(client: &Arc, node: &NodeRef<'_ client .core .event_bus - .dispatch(&Event::DisappearingModeChanged( + .dispatch(Event::DisappearingModeChanged( wacore::types::events::DisappearingModeChanged { from, duration, @@ -1425,10 +1425,10 @@ fn handle_disappearing_mode_notification(client: &Arc, node: &NodeRef<'_ #[cfg(test)] mod tests { use super::*; - use crate::test_utils::create_test_client; - use std::sync::{Arc, Mutex}; + use crate::test_utils::{TestEventCollector, create_test_client}; + use std::sync::Arc; use wacore::stanza::devices::DeviceNotificationType; - use wacore::types::events::{DeviceListUpdateType, EventHandler}; + use wacore::types::events::DeviceListUpdateType; use wacore_binary::Node; use wacore_binary::builder::NodeBuilder; @@ -1436,29 +1436,6 @@ mod tests { crate::test_utils::node_to_owned_ref(&node) } - #[derive(Default)] - struct TestEventCollector { - events: Mutex>, - } - - impl EventHandler for TestEventCollector { - fn handle_event(&self, event: &Event) { - self.events - .lock() - .expect("collector mutex should not be poisoned") - .push(event.clone()); - } - } - - impl TestEventCollector { - fn events(&self) -> Vec { - self.events - .lock() - .expect("collector mutex should not be poisoned") - .clone() - } - } - #[test] fn test_parse_device_add_notification() { // Per WhatsApp Web: add operation has single device + key-index-list @@ -1829,11 +1806,14 @@ mod tests { handle_notification_impl(&client, node_to_arc(node)).await; let events = collector.events(); - assert!(matches!( - events.as_slice(), - [Event::ContactUpdated(ContactUpdated { jid, .. })] - if jid == &Jid::pn("5511999999999") - )); + assert!( + events.len() == 1 + && matches!( + &*events[0], + Event::ContactUpdated(ContactUpdated { jid, .. }) + if jid == &Jid::pn("5511999999999") + ) + ); } #[tokio::test] @@ -1877,16 +1857,19 @@ mod tests { ); let events = collector.events(); - assert!(matches!( - events.as_slice(), - [Event::ContactNumberChanged(ContactNumberChanged { - old_jid, new_jid, old_lid, new_lid, .. - })] - if old_jid == &Jid::pn("5511999999999") - && new_jid == &Jid::pn("5511888888888") - && old_lid.is_some() - && new_lid.is_some() - )); + assert!( + events.len() == 1 + && matches!( + &*events[0], + Event::ContactNumberChanged(ContactNumberChanged { + old_jid, new_jid, old_lid, new_lid, .. + }) + if old_jid == &Jid::pn("5511999999999") + && new_jid == &Jid::pn("5511888888888") + && old_lid.is_some() + && new_lid.is_some() + ) + ); } #[tokio::test] @@ -1927,11 +1910,14 @@ mod tests { handle_notification_impl(&client, node_to_arc(node)).await; let events = collector.events(); - assert!(matches!( - events.as_slice(), - [Event::ContactSyncRequested(ContactSyncRequested { after, .. })] - if after.is_some() - )); + assert!( + events.len() == 1 + && matches!( + &*events[0], + Event::ContactSyncRequested(ContactSyncRequested { after, .. }) + if after.is_some() + ) + ); } #[tokio::test] @@ -2033,7 +2019,9 @@ mod tests { // Should have dispatched IdentityChange event let events = collector.events(); assert!( - events.iter().any(|e| matches!(e, Event::IdentityChange(_))), + events + .iter() + .any(|e| matches!(&**e, Event::IdentityChange(_))), "should dispatch IdentityChange event, got: {:?}", events ); @@ -2204,7 +2192,7 @@ mod tests { collector .events() .iter() - .any(|e| matches!(e, Event::IdentityChange(_))), + .any(|e| matches!(&**e, Event::IdentityChange(_))), "offline identity change should still dispatch event" ); } diff --git a/src/handlers/presence.rs b/src/handlers/presence.rs index 54ca5bc28..65f70c6f5 100644 --- a/src/handlers/presence.rs +++ b/src/handlers/presence.rs @@ -55,7 +55,7 @@ impl StanzaHandler for PresenceHandler { client .core .event_bus - .dispatch(&Event::Presence(PresenceUpdate { + .dispatch(Event::Presence(PresenceUpdate { from: from_jid, unavailable, last_seen, diff --git a/src/history_sync.rs b/src/history_sync.rs index 164ff78dc..0fedecf86 100644 --- a/src/history_sync.rs +++ b/src/history_sync.rs @@ -222,7 +222,7 @@ impl Client { // Wrap Bytes in LazyConversation using from_bytes (true zero-copy) // Parsing only happens if the event handler calls .conversation() or .get() let lazy_conv = LazyConversation::from_bytes(raw_bytes); - self.core.event_bus.dispatch(&Event::JoinedGroup(lazy_conv)); + self.core.event_bus.dispatch(Event::JoinedGroup(lazy_conv)); } // Drop receiver before awaiting the blocking task. If we broke out diff --git a/src/main.rs b/src/main.rs index 53efa9faa..9ca14fdaf 100644 --- a/src/main.rs +++ b/src/main.rs @@ -82,7 +82,7 @@ fn main() { let mut bot = builder .on_event(move |event, client| async move { - match event { + match &*event { Event::PairingQrCode { code, timeout } => { info!("----------------------------------------"); info!( @@ -104,12 +104,15 @@ fn main() { info!("========================================"); } Event::Message(msg, info) => { - let ctx = MessageContext { - message: msg, - info, - client, - }; - handle_message(&ctx).await; + let ctx = MessageContext::from_parts(msg, info, client); + if let Some(reply) = build_media_pong(msg) { + info!("Received media ping from {}", ctx.info.source.sender); + if let Err(e) = ctx.send_message(reply).await { + error!("Failed to send media pong: {}", e); + } + } else if msg.text_content() == Some(PING_TRIGGER) { + handle_text_ping(&ctx).await; + } } Event::Connected(_) => info!("✅ Bot connected successfully!"), Event::LoggedOut(_) => error!("❌ Bot was logged out!"), @@ -150,20 +153,6 @@ fn main() { }); } -async fn handle_message(ctx: &MessageContext) { - if let Some(reply) = build_media_pong(&ctx.message) { - info!("Received media ping from {}", ctx.info.source.sender); - if let Err(e) = ctx.send_message(reply).await { - error!("Failed to send media pong: {}", e); - } - return; - } - - if ctx.message.text_content() == Some(PING_TRIGGER) { - handle_text_ping(ctx).await; - } -} - async fn handle_text_ping(ctx: &MessageContext) { info!("Received text ping, sending pong..."); diff --git a/src/message.rs b/src/message.rs index 09431a517..c8e0958a6 100644 --- a/src/message.rs +++ b/src/message.rs @@ -54,7 +54,7 @@ impl Client { self.core .event_bus - .dispatch(&Event::Message(Box::new(msg), info)); + .dispatch(Event::Message(Box::new(msg), info)); } /// Handles a newsletter plaintext message. @@ -100,7 +100,7 @@ impl Client { info: &MessageInfo, decrypt_fail_mode: crate::types::events::DecryptFailMode, ) { - self.core.event_bus.dispatch(&Event::UndecryptableMessage( + self.core.event_bus.dispatch(Event::UndecryptableMessage( crate::types::events::UndecryptableMessage { info: info.clone(), is_unavailable: false, @@ -320,7 +320,7 @@ impl Client { ); // Ack is handled by the framework; PDO asks the primary phone to relay the message self.spawn_pdo_request_with_options(&info, true); - self.core.event_bus.dispatch(&Event::UndecryptableMessage( + self.core.event_bus.dispatch(Event::UndecryptableMessage( crate::types::events::UndecryptableMessage { info: (*info).clone(), is_unavailable: true, diff --git a/src/pair.rs b/src/pair.rs index a2e0488e6..327b3ce98 100644 --- a/src/pair.rs +++ b/src/pair.rs @@ -85,7 +85,7 @@ pub async fn handle_iq(client: &Arc, node: &NodeRef<'_>) -> bool { client_clone .core .event_bus - .dispatch(&Event::PairingQrCode { code, timeout }); + .dispatch(Event::PairingQrCode { code, timeout }); let sleep = client_clone.runtime.sleep(timeout); let stop = stop_rx.recv(); @@ -115,9 +115,6 @@ pub async fn handle_iq(client: &Arc, node: &NodeRef<'_>) -> bool { .detach(); *client.pairing_cancellation_tx.lock().await = Some(stop_tx); - - // We no longer dispatch the raw Event::Qr - // client.core.event_bus.dispatch(&Event::Qr(Qr { codes })); true } "pair-success" => { @@ -221,7 +218,7 @@ async fn handle_pair_success<'a>( error!( "FATAL: Failed to re-decode self-signed identity for event, pairing cannot complete: {e}" ); - client.core.event_bus.dispatch(&Event::PairError(PairError { + client.core.event_bus.dispatch(Event::PairError(PairError { id: jid.clone(), lid: lid.clone(), business_name: business_name.clone(), @@ -324,7 +321,7 @@ async fn handle_pair_success<'a>( client .core .event_bus - .dispatch(&Event::PairSuccess(success_event)); + .dispatch(Event::PairSuccess(success_event)); } Err(e) => { error!("Pairing crypto failed: {e}"); @@ -343,7 +340,7 @@ async fn handle_pair_success<'a>( client .core .event_bus - .dispatch(&Event::PairError(pair_error_event)); + .dispatch(Event::PairError(pair_error_event)); } } } diff --git a/src/pair_code.rs b/src/pair_code.rs index 53b48574d..78f7b8e2a 100644 --- a/src/pair_code.rs +++ b/src/pair_code.rs @@ -212,7 +212,7 @@ impl Client { }; // Dispatch event for user to display the code - self.core.event_bus.dispatch(&Event::PairingCode { + self.core.event_bus.dispatch(Event::PairingCode { code: code.clone(), timeout: PairCodeUtils::code_validity(), }); diff --git a/src/pdo.rs b/src/pdo.rs index 88ab37b75..703dd10be 100644 --- a/src/pdo.rs +++ b/src/pdo.rs @@ -371,7 +371,7 @@ impl Client { self.core .event_bus - .dispatch(&wacore::types::events::Event::Message( + .dispatch(wacore::types::events::Event::Message( Box::new(message), message_info, )); diff --git a/src/receipt.rs b/src/receipt.rs index 6b76f47bb..4798ac96f 100644 --- a/src/receipt.rs +++ b/src/receipt.rs @@ -108,9 +108,9 @@ impl Client { .unwrap_or(1), ); } - self.core.event_bus.dispatch(&Event::Receipt(receipt)); + self.core.event_bus.dispatch(Event::Receipt(receipt)); } else { - self.core.event_bus.dispatch(&Event::Receipt(receipt)); + self.core.event_bus.dispatch(Event::Receipt(receipt)); } } @@ -204,38 +204,13 @@ impl Client { mod tests { use super::*; use crate::store::persistence_manager::PersistenceManager; - use crate::test_utils::MockHttpClient; + use crate::test_utils::{MockHttpClient, TestEventCollector}; use crate::types::message::{MessageInfo, MessageSource}; - use std::sync::Mutex; - use wacore::types::events::EventHandler; fn node_to_arc(node: wacore_binary::Node) -> Arc { crate::test_utils::node_to_owned_ref(&node) } - #[derive(Default)] - struct TestEventCollector { - events: Mutex>, - } - - impl EventHandler for TestEventCollector { - fn handle_event(&self, event: &Event) { - self.events - .lock() - .expect("collector mutex should not be poisoned") - .push(event.clone()); - } - } - - impl TestEventCollector { - fn events(&self) -> Vec { - self.events - .lock() - .expect("collector mutex should not be poisoned") - .clone() - } - } - #[tokio::test] async fn test_send_delivery_receipt_dm() { let backend = crate::test_utils::create_test_backend().await; @@ -534,7 +509,7 @@ mod tests { let events = collector.events(); let receipt_events: Vec<_> = events .iter() - .filter_map(|e| match e { + .filter_map(|e| match &**e { Event::Receipt(r) => Some(r), _ => None, }) @@ -573,7 +548,7 @@ mod tests { let events = collector.events(); let receipt_events: Vec<_> = events .iter() - .filter_map(|e| match e { + .filter_map(|e| match &**e { Event::Receipt(r) => Some(r), _ => None, }) diff --git a/src/test_utils.rs b/src/test_utils.rs index f37fcd962..f3811c645 100644 --- a/src/test_utils.rs +++ b/src/test_utils.rs @@ -121,6 +121,32 @@ pub async fn create_test_client_with_failing_http(name: &str) -> Arc { client } +use std::sync::Mutex; +use wacore::types::events::{Event, EventHandler}; + +#[derive(Default)] +pub struct TestEventCollector { + events: Mutex>>, +} + +impl EventHandler for TestEventCollector { + fn handle_event(&self, event: Arc) { + self.events + .lock() + .expect("collector mutex should not be poisoned") + .push(event); + } +} + +impl TestEventCollector { + pub fn events(&self) -> Vec> { + self.events + .lock() + .expect("collector mutex should not be poisoned") + .clone() + } +} + pub async fn create_test_backend() -> Arc { use portable_atomic::AtomicU64; use std::sync::atomic::Ordering; diff --git a/tests/e2e/src/lib.rs b/tests/e2e/src/lib.rs index fbb41f9d3..c246c47f8 100644 --- a/tests/e2e/src/lib.rs +++ b/tests/e2e/src/lib.rs @@ -45,7 +45,7 @@ pub fn scenario_push_name(prefix: &str, flags: &[&str]) -> String { /// A connected client ready for testing, with its event receiver and run handle. pub struct TestClient { pub client: Arc, - pub event_rx: async_channel::Receiver, + pub event_rx: async_channel::Receiver>, pub run_handle: whatsapp_rust::bot::BotHandle, } @@ -104,13 +104,13 @@ impl TestClient { let wait_result = tokio::time::timeout(timeout, async { loop { match event_rx.recv().await { - Ok(Event::PairSuccess(_)) => { + Ok(ref event) if matches!(**event, Event::PairSuccess(_)) => { got_pair = true; if got_connected { break; } } - Ok(Event::Connected(_)) => { + Ok(ref event) if matches!(**event, Event::Connected(_)) => { got_connected = true; if got_pair { break; @@ -150,7 +150,7 @@ impl TestClient { let _ = tokio::time::timeout(connected_timeout, async { loop { match event_rx.recv().await { - Ok(Event::Connected(_)) => break, + Ok(ref event) if matches!(**event, Event::Connected(_)) => break, Ok(_) => continue, Err(_) => break, } @@ -288,7 +288,7 @@ impl TestClient { &mut self, timeout_secs: u64, mut predicate: F, - ) -> anyhow::Result + ) -> anyhow::Result> where F: FnMut(&Event) -> bool, { @@ -307,14 +307,14 @@ impl TestClient { } /// Wait for a text message with specific content. - pub async fn wait_for_text(&mut self, text: &str, timeout_secs: u64) -> anyhow::Result { + pub async fn wait_for_text( + &mut self, + text: &str, + timeout_secs: u64, + ) -> anyhow::Result> { let text = text.to_string(); self.wait_for_event(timeout_secs, move |e| { - matches!( - e, - Event::Message(msg, _) - if msg.conversation.as_deref() == Some(text.as_str()) - ) + e.message_text() == Some(text.as_str()) }) .await } @@ -325,7 +325,7 @@ impl TestClient { group_jid: &Jid, text: &str, timeout_secs: u64, - ) -> anyhow::Result { + ) -> anyhow::Result> { let gid = group_jid.clone(); let text = text.to_string(); self.wait_for_event(timeout_secs, move |e| { @@ -343,7 +343,7 @@ impl TestClient { pub async fn wait_for_group_notification( &mut self, timeout_secs: u64, - ) -> anyhow::Result { + ) -> anyhow::Result> { self.wait_for_event(timeout_secs, |e| { matches!(e, Event::Notification(node) if node.get().get_attr("type").is_some_and(|v| v.as_str() == "w:gp2")) }) @@ -392,7 +392,7 @@ impl TestClient { pub async fn reconnect_and_wait(&mut self) -> anyhow::Result<()> { // Drain any buffered Connected events from prior connections while let Ok(event) = self.event_rx.try_recv() { - if matches!(event, Event::Connected(_)) { + if matches!(&*event, Event::Connected(_)) { continue; } } diff --git a/tests/e2e/tests/app_state.rs b/tests/e2e/tests/app_state.rs index 60e4c868e..2c05ced55 100644 --- a/tests/e2e/tests/app_state.rs +++ b/tests/e2e/tests/app_state.rs @@ -222,7 +222,7 @@ async fn test_star_received_message() -> anyhow::Result<()> { }) .await?; - let msg_id = if let Event::Message(_, info) = &event { + let msg_id = if let Event::Message(_, info) = &*event { info.id.clone() } else { panic!("Expected Message event"); @@ -295,7 +295,7 @@ async fn test_multi_device_app_state_sync() -> anyhow::Result<()> { .wait_for_event(15, |e| matches!(e, Event::SelfPushNameUpdated(_))) .await?; - if let Event::SelfPushNameUpdated(update) = &event { + if let Event::SelfPushNameUpdated(update) = &*event { assert_eq!( update.new_name, *new_name, "A2 should receive the updated push name from A1" diff --git a/tests/e2e/tests/media.rs b/tests/e2e/tests/media.rs index 67f5d178d..979b0c9f7 100644 --- a/tests/e2e/tests/media.rs +++ b/tests/e2e/tests/media.rs @@ -418,7 +418,7 @@ async fn test_send_image_message() -> anyhow::Result<()> { ) .await?; - if let Event::Message(msg, info) = event { + if let Event::Message(msg, info) = &*event { let img = msg.image_message.as_ref().unwrap(); assert_eq!(img.caption.as_deref(), Some(caption)); assert_eq!(img.mimetype.as_deref(), Some("image/jpeg")); @@ -476,7 +476,7 @@ async fn test_send_video_message() -> anyhow::Result<()> { ) .await?; - if let Event::Message(msg, _) = event { + if let Event::Message(msg, _) = &*event { let vid = msg.video_message.as_ref().unwrap(); assert_eq!(vid.caption.as_deref(), Some("Cool video")); assert_eq!(vid.seconds, Some(15)); @@ -526,7 +526,7 @@ async fn test_send_document_message() -> anyhow::Result<()> { ) .await?; - if let Event::Message(msg, _) = event { + if let Event::Message(msg, _) = &*event { let doc = msg.document_message.as_ref().unwrap(); assert_eq!(doc.file_name.as_deref(), Some("report.pdf")); assert_eq!(doc.mimetype.as_deref(), Some("application/pdf")); @@ -575,7 +575,7 @@ async fn test_send_audio_message() -> anyhow::Result<()> { ) .await?; - if let Event::Message(msg, _) = event { + if let Event::Message(msg, _) = &*event { let audio = msg.audio_message.as_ref().unwrap(); assert_eq!(audio.seconds, Some(30)); assert_eq!(audio.ptt, Some(false)); @@ -624,7 +624,7 @@ async fn test_send_ptt_voice_message() -> anyhow::Result<()> { ) .await?; - if let Event::Message(msg, _) = event { + if let Event::Message(msg, _) = &*event { let audio = msg.audio_message.as_ref().unwrap(); assert_eq!(audio.ptt, Some(true)); assert_eq!(audio.seconds, Some(5)); @@ -682,7 +682,7 @@ async fn test_send_image_bidirectional() -> anyhow::Result<()> { |e| matches!(e, Event::Message(m, _) if m.image_message.is_some()), ) .await?; - if let Event::Message(msg, _) = event { + if let Event::Message(msg, _) = &*event { let img = msg.image_message.as_ref().unwrap(); assert_eq!(img.caption.as_deref(), Some("From A")); let downloaded = client_b @@ -708,7 +708,7 @@ async fn test_send_image_bidirectional() -> anyhow::Result<()> { |e| matches!(e, Event::Message(m, _) if m.image_message.is_some()), ) .await?; - if let Event::Message(msg, _) = event { + if let Event::Message(msg, _) = &*event { let img = msg.image_message.as_ref().unwrap(); assert_eq!(img.caption.as_deref(), Some("From B")); let downloaded = client_a @@ -754,7 +754,7 @@ async fn test_send_multiple_media_types() -> anyhow::Result<()> { |e| matches!(e, Event::Message(m, _) if m.image_message.is_some()), ) .await?; - if let Event::Message(msg, _) = event { + if let Event::Message(msg, _) = &*event { let img = msg.image_message.as_ref().unwrap(); let dl = client_b .client @@ -778,7 +778,7 @@ async fn test_send_multiple_media_types() -> anyhow::Result<()> { |e| matches!(e, Event::Message(m, _) if m.document_message.is_some()), ) .await?; - if let Event::Message(msg, _) = event { + if let Event::Message(msg, _) = &*event { let doc = msg.document_message.as_ref().unwrap(); let dl = client_b .client @@ -802,7 +802,7 @@ async fn test_send_multiple_media_types() -> anyhow::Result<()> { |e| matches!(e, Event::Message(m, _) if m.audio_message.is_some()), ) .await?; - if let Event::Message(msg, _) = event { + if let Event::Message(msg, _) = &*event { let audio = msg.audio_message.as_ref().unwrap(); let dl = client_b .client @@ -919,7 +919,7 @@ async fn test_send_image_no_caption() -> anyhow::Result<()> { ) .await?; - if let Event::Message(msg, _) = event { + if let Event::Message(msg, _) = &*event { let img = msg.image_message.as_ref().unwrap(); assert!( img.caption.is_none() || img.caption.as_deref() == Some(""), diff --git a/tests/e2e/tests/messaging.rs b/tests/e2e/tests/messaging.rs index 0329029fe..2a51eb66c 100644 --- a/tests/e2e/tests/messaging.rs +++ b/tests/e2e/tests/messaging.rs @@ -97,7 +97,7 @@ async fn test_message_revoke() -> anyhow::Result<()> { ) .await?; - if let Event::Message(msg, _) = event { + if let Event::Message(msg, _) = &*event { let proto = msg.protocol_message.as_ref().unwrap(); assert_eq!( proto.r#type(), @@ -139,7 +139,7 @@ async fn test_message_has_push_name() -> anyhow::Result<()> { // Assert the push_name field on the received event let event = client_b.wait_for_text(text, 15).await?; - if let Event::Message(_, info) = event { + if let Event::Message(_, info) = &*event { assert_eq!( info.push_name, push_name, "Received message push_name should match the sender's display name" diff --git a/tests/e2e/tests/newsletter.rs b/tests/e2e/tests/newsletter.rs index f95980e00..38a8f6df2 100644 --- a/tests/e2e/tests/newsletter.rs +++ b/tests/e2e/tests/newsletter.rs @@ -457,7 +457,7 @@ async fn test_newsletter_reaction_live_update() -> anyhow::Result<()> { }) .await?; - if let Event::NewsletterLiveUpdate(update) = event { + if let Event::NewsletterLiveUpdate(update) = &*event { info!( "Received live update for {} with {} message(s)", update.newsletter_jid, diff --git a/tests/e2e/tests/offline_groups.rs b/tests/e2e/tests/offline_groups.rs index 22ac8d374..d719348ed 100644 --- a/tests/e2e/tests/offline_groups.rs +++ b/tests/e2e/tests/offline_groups.rs @@ -149,17 +149,17 @@ async fn test_mixed_offline_event_ordering() -> anyhow::Result<()> { .await; match result { - Ok(Event::Message(msg, _)) => { - let text = msg.conversation.unwrap_or_default(); + Ok(ref event) if let Some((msg, _)) = event.as_message() => { + let text = msg.conversation.clone().unwrap_or_default(); info!("C received message: {text}"); messages_received.push(text); } - Ok(Event::Notification(_)) => { + Ok(ref event) if matches!(**event, Event::Notification(_)) => { info!("C received group notification"); notifications_received += 1; } Ok(_) => {} - Err(_) => break, // timeout — no more events + Err(_) => break, } } @@ -239,7 +239,7 @@ async fn test_offline_group_message_delivery() -> anyhow::Result<()> { // C should receive it after reconnecting (from offline queue) let event = client_c.wait_for_text(text, 30).await?; - if let Event::Message(msg, info) = event { + if let Event::Message(msg, info) = &*event { assert_eq!(msg.conversation.as_deref(), Some(text)); assert!(info.source.is_group); assert_eq!(info.source.chat, group_jid); @@ -438,17 +438,15 @@ async fn test_offline_multi_sender_group_messages() -> anyhow::Result<()> { .await; match result { - Ok(Event::Message(msg, _)) => { - if let Some(text) = msg.conversation { + Ok(ref event) if let Some((msg, _)) = event.as_message() => { + if let Some(text) = &msg.conversation { info!("C received: {text}"); - received_texts.insert(text); + received_texts.insert(text.clone()); } } - Ok(Event::Notification(_)) => { - // Group notifications are expected, just skip - } + Ok(ref event) if matches!(**event, Event::Notification(_)) => {} Ok(_) => {} - Err(_) => break, // no more events within timeout + Err(_) => break, } // Early exit once all expected messages are collected diff --git a/tests/e2e/tests/offline_messages.rs b/tests/e2e/tests/offline_messages.rs index 9eaa1e394..275738118 100644 --- a/tests/e2e/tests/offline_messages.rs +++ b/tests/e2e/tests/offline_messages.rs @@ -66,8 +66,8 @@ async fn test_offline_message_ordering() -> anyhow::Result<()> { ) .await?; - if let Event::Message(msg, _) = event { - let text = msg.conversation.unwrap(); + if let Event::Message(msg, _) = &*event { + let text = msg.conversation.clone().unwrap(); info!("Received: {text}"); received.push(text); } diff --git a/tests/e2e/tests/offline_receipts.rs b/tests/e2e/tests/offline_receipts.rs index 6accc9f95..4a3afa7e2 100644 --- a/tests/e2e/tests/offline_receipts.rs +++ b/tests/e2e/tests/offline_receipts.rs @@ -200,7 +200,7 @@ async fn test_offline_presence_coalescing() -> anyhow::Result<()> { .wait_for_event(30, |e| matches!(e, Event::Presence(_))) .await?; - if let Event::Presence(presence) = &presence_event { + if let Event::Presence(presence) = &*presence_event { info!("B received coalesced presence: {:?}", presence); } diff --git a/tests/e2e/tests/prekey_sessions.rs b/tests/e2e/tests/prekey_sessions.rs index 77dcb2a1e..3b9a5c093 100644 --- a/tests/e2e/tests/prekey_sessions.rs +++ b/tests/e2e/tests/prekey_sessions.rs @@ -154,7 +154,7 @@ async fn test_prekey_collision_regression() -> anyhow::Result<()> { break; } match tokio::time::timeout(remaining, recipient.event_rx.recv()).await { - Ok(Ok(Event::Connected(_))) => { + Ok(Ok(ref event)) if matches!(**event, Event::Connected(_)) => { got_connected = true; } Ok(Ok(_)) => {} // Skip messages, undecryptable, etc. diff --git a/tests/e2e/tests/profile.rs b/tests/e2e/tests/profile.rs index ae99f2426..24ddc1306 100644 --- a/tests/e2e/tests/profile.rs +++ b/tests/e2e/tests/profile.rs @@ -275,7 +275,7 @@ async fn test_status_text_notification_received() -> anyhow::Result<()> { ) .await?; - if let Event::UserAboutUpdate(update) = event { + if let Event::UserAboutUpdate(update) = &*event { info!( "Client B received status update from {} (length={})", update.jid, diff --git a/tests/e2e/tests/profile_picture.rs b/tests/e2e/tests/profile_picture.rs index ce14af5d5..edd7ad40d 100644 --- a/tests/e2e/tests/profile_picture.rs +++ b/tests/e2e/tests/profile_picture.rs @@ -50,7 +50,7 @@ async fn test_set_profile_picture() -> anyhow::Result<()> { |e| matches!(e, Event::PictureUpdate(u) if !u.removed && u.jid == expected_jid), ) .await?; - if let Event::PictureUpdate(update) = event { + if let Event::PictureUpdate(update) = &*event { info!( "Received PictureUpdate event: jid={}, author={:?}, removed={}, pic_id={:?}", update.jid, update.author, update.removed, update.picture_id @@ -148,7 +148,7 @@ async fn test_remove_profile_picture() -> anyhow::Result<()> { |e| matches!(e, Event::PictureUpdate(u) if u.removed && u.jid == expected_jid), ) .await?; - if let Event::PictureUpdate(update) = event { + if let Event::PictureUpdate(update) = &*event { assert!( update.picture_id.is_none(), "Delete should have no picture ID" diff --git a/tests/e2e/tests/session_reuse.rs b/tests/e2e/tests/session_reuse.rs index 1b962fd93..c4ef23726 100644 --- a/tests/e2e/tests/session_reuse.rs +++ b/tests/e2e/tests/session_reuse.rs @@ -370,7 +370,7 @@ async fn test_message_info_fields() -> anyhow::Result<()> { let event = client_b.wait_for_text(text_ab, 30).await?; - if let Event::Message(msg, info) = event { + if let Event::Message(msg, info) = &*event { assert_eq!(msg.conversation.as_deref(), Some(text_ab)); assert!(!info.id.is_empty(), "Message ID must not be empty"); assert!( @@ -406,7 +406,7 @@ async fn test_message_info_fields() -> anyhow::Result<()> { let event = client_a.wait_for_text(text_ba, 30).await?; - if let Event::Message(_, info) = event { + if let Event::Message(_, info) = &*event { assert!(!info.source.is_from_me); assert!(!info.source.is_group); assert_eq!(info.source.sender.user, jid_b.user); diff --git a/wacore/src/types/events.rs b/wacore/src/types/events.rs index debc0373c..5c94ed124 100644 --- a/wacore/src/types/events.rs +++ b/wacore/src/types/events.rs @@ -12,50 +12,11 @@ use wacore_binary::OwnedNodeRef; use wacore_binary::{Jid, MessageId}; use waproto::whatsapp::{self as wa, HistorySync}; -/// Wrapper for large event data that uses Arc for cheap cloning. -/// This avoids cloning large protobuf messages when dispatching events. -#[derive(Debug, Clone)] -pub struct SharedData(pub Arc); - -impl SharedData { - pub fn new(data: T) -> Self { - Self(Arc::new(data)) - } -} - -impl std::ops::Deref for SharedData { - type Target = T; - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl Serialize for SharedData { - fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, - { - self.0.serialize(serializer) - } -} - /// A lazily-parsed conversation from history sync. /// -/// The raw protobuf bytes are stored and only parsed when accessed. -/// This allows emitting events without the cost of parsing if the -/// consumer doesn't actually need the conversation data. -/// -/// Uses `bytes::Bytes` for zero-copy reference counting. Cloning is O(1) -/// and parsing only happens once on first access. -/// -/// Clones get their own `OnceLock` (no `Arc` overhead). This is correct -/// because the original is dropped right after event dispatch — only the -/// cloned copy in the spawned handler task ever calls `.get()`. -/// -/// **Multi-handler note**: if the event bus fans out to N handlers, each -/// clone parses independently. This is acceptable because parsing is -/// idempotent and the common case is a single handler. If multi-handler -/// parsing cost becomes an issue, wrap `parsed` in `Arc>`. +/// Raw protobuf bytes are stored and only parsed on first access. +/// With `Arc` dispatch, all handlers share the same `LazyConversation` +/// so `OnceLock` gives parse-once semantics for free. #[derive(Clone)] pub struct LazyConversation { /// Raw protobuf bytes using Bytes for zero-copy cloning. @@ -169,40 +130,33 @@ impl Serialize for LazyConversation { } pub trait EventHandler: Send + Sync { - fn handle_event(&self, event: &Event); + fn handle_event(&self, event: Arc); } -/// Event handler that forwards events to an async channel for test assertions. -/// -/// Uses `async_channel` (runtime-agnostic) so it works with any async executor. -/// Events are buffered in an unbounded channel, so events fired before the -/// receiver starts listening are not lost. +/// Event handler that forwards events to an async channel. /// /// # Example /// ```ignore -/// let (handler, mut rx) = ChannelEventHandler::new(); +/// let (handler, rx) = ChannelEventHandler::new(); /// client.register_handler(handler); -/// // ... start the client ... -/// // Wait for Connected event /// while let Ok(event) = rx.recv().await { -/// if matches!(event, Event::Connected(_)) { break; } +/// if matches!(&*event, Event::Connected(_)) { break; } /// } /// ``` pub struct ChannelEventHandler { - tx: async_channel::Sender, + tx: async_channel::Sender>, } impl ChannelEventHandler { - /// Create a new handler and its event receiver. - pub fn new() -> (Arc, async_channel::Receiver) { + pub fn new() -> (Arc, async_channel::Receiver>) { let (tx, rx) = async_channel::unbounded(); (Arc::new(Self { tx }), rx) } } impl EventHandler for ChannelEventHandler { - fn handle_event(&self, event: &Event) { - let _ = self.tx.try_send(event.clone()); + fn handle_event(&self, event: Arc) { + let _ = self.tx.try_send(event); } } @@ -233,14 +187,18 @@ impl CoreEventBus { .is_empty() } - pub fn dispatch(&self, event: &Event) { - for handler in self + pub fn dispatch(&self, event: Event) { + let handlers = self .handlers .read() .expect("RwLock should not be poisoned") - .iter() - { - handler.handle_event(event); + .clone(); + if handlers.is_empty() { + return; + } + let event = Arc::new(event); + for handler in &handlers { + handler.handle_event(Arc::clone(&event)); } } } @@ -474,6 +432,21 @@ pub enum Event { RawNode(Arc), } +impl Event { + pub fn as_message(&self) -> Option<(&wa::Message, &MessageInfo)> { + if let Event::Message(msg, info) = self { + Some((msg, info)) + } else { + None + } + } + + pub fn message_text(&self) -> Option<&str> { + let (msg, _) = self.as_message()?; + msg.conversation.as_deref() + } +} + /// A newsletter live update notification, typically containing updated /// reaction counts for one or more messages. #[derive(Debug, Clone, Serialize)]