1use crate::cache_config::CacheConfig;
2use crate::client::{Client, ClientBuilderError};
3use crate::pair_code::PairCodeOptions;
4#[cfg(feature = "plugins")]
5use crate::plugins::{ClientPlugin, PluginHostConfig, PluginRegistration, UntypedClientPlugin};
6use crate::store::commands::DeviceCommand;
7use crate::store::error::StoreError;
8use crate::store::persistence_manager::PersistenceManager;
9use crate::store::traits::Backend;
10use crate::types::durability_hook::InboundDurabilityHook;
11use crate::types::enc_handler::EncHandler;
12use crate::types::events::{Event, EventHandler, EventInterest, EventKind};
13use crate::types::message::MessageInfo;
14use futures::FutureExt;
15use log::{info, warn};
16use std::collections::HashMap;
17use std::future::Future;
18use std::marker::PhantomData;
19use std::pin::Pin;
20use std::sync::{Arc, Weak};
21use thiserror::Error;
22use wacore::proto_helpers::MessageBuilderExt;
23use wacore::runtime::Runtime;
24use wacore::store::DevicePropsOverride;
25use waproto::whatsapp as wa;
26
27pub struct Provided;
30pub struct MissingBackend;
32pub struct MissingTransport;
35pub struct MissingHttpClient;
38pub struct MissingRuntime;
41
42#[cfg(feature = "tokio-transport")]
43type DefaultTransportState = Provided;
44#[cfg(not(feature = "tokio-transport"))]
45type DefaultTransportState = MissingTransport;
46
47#[cfg(feature = "ureq-client")]
48type DefaultHttpState = Provided;
49#[cfg(not(feature = "ureq-client"))]
50type DefaultHttpState = MissingHttpClient;
51
52#[cfg(feature = "tokio-runtime")]
53type DefaultRuntimeState = Provided;
54#[cfg(not(feature = "tokio-runtime"))]
55type DefaultRuntimeState = MissingRuntime;
56
57#[cfg(feature = "tokio-transport")]
58fn default_transport_factory() -> Option<Arc<dyn crate::transport::TransportFactory>> {
59 Some(Arc::new(
60 crate::transport::TokioWebSocketTransportFactory::new(),
61 ))
62}
63#[cfg(not(feature = "tokio-transport"))]
64fn default_transport_factory() -> Option<Arc<dyn crate::transport::TransportFactory>> {
65 None
66}
67
68#[cfg(feature = "ureq-client")]
69fn default_http_client() -> Option<Arc<dyn crate::http::HttpClient>> {
70 Some(Arc::new(crate::http::UreqHttpClient::new()))
71}
72#[cfg(not(feature = "ureq-client"))]
73fn default_http_client() -> Option<Arc<dyn crate::http::HttpClient>> {
74 None
75}
76
77#[cfg(feature = "tokio-runtime")]
78fn default_runtime() -> Option<Arc<dyn Runtime>> {
79 Some(Arc::new(crate::runtime_impl::TokioRuntime))
80}
81#[cfg(not(feature = "tokio-runtime"))]
82fn default_runtime() -> Option<Arc<dyn Runtime>> {
83 None
84}
85
86#[derive(Debug, Error)]
87#[non_exhaustive]
88pub enum BotBuilderError {
89 #[error("failed to initialize the device store: {0}")]
91 Store(#[from] StoreError),
92 #[error("{0}")]
93 Client(#[from] ClientBuilderError),
94}
95
96#[derive(Clone)]
100pub struct MessageContext {
101 pub message: Arc<wa::Message>,
102 pub info: MessageInfo,
103 pub client: Arc<Client>,
104}
105
106impl MessageContext {
107 pub fn from_parts(message: &wa::Message, info: &MessageInfo, client: Arc<Client>) -> Self {
111 Self::from_arc(Arc::new(message.clone()), info, client)
112 }
113
114 pub fn from_arc(message: Arc<wa::Message>, info: &MessageInfo, client: Arc<Client>) -> Self {
115 Self {
116 message,
117 info: info.clone(),
118 client,
119 }
120 }
121
122 pub fn from_inbound(
123 inbound: &wacore::types::events::InboundMessage,
124 client: Arc<Client>,
125 ) -> Self {
126 Self::from_arc(Arc::clone(&inbound.message), &inbound.info, client)
127 }
128
129 #[cfg_attr(feature = "tracing", tracing::instrument(name = "wa.bot.send_message", level = "debug", skip_all, fields(chat = %self.info.source.chat.observe()), err(Debug)))]
130 pub async fn send_message(
131 &self,
132 message: wa::Message,
133 ) -> Result<crate::send::SendResult, crate::send::SendError> {
134 self.client
135 .send_message(&self.info.source.chat, message)
136 .await
137 }
138
139 pub async fn reply(
141 &self,
142 text: impl Into<String>,
143 ) -> Result<crate::send::SendResult, crate::send::SendError> {
144 self.send_message(wa::Message::text(text)).await
145 }
146
147 pub async fn reply_quoting(
149 &self,
150 text: impl Into<String>,
151 ) -> Result<crate::send::SendResult, crate::send::SendError> {
152 let context = self.build_quote_context();
153 self.send_message(wa::Message::text_with_context(text, context))
154 .await
155 }
156
157 pub fn build_quote_context(&self) -> wa::ContextInfo {
158 let chat = &self.info.source.chat;
161 wacore::proto_helpers::build_quote_context_with_info(
162 &self.info.id,
163 &self.info.source.sender,
164 chat,
165 chat,
166 &self.message,
167 )
168 }
169
170 pub fn message_key(&self) -> wa::MessageKey {
173 use wacore_binary::JidExt;
174 let needs_participant =
175 self.info.source.is_group || self.info.source.chat.is_status_broadcast();
176 wa::MessageKey {
177 remote_jid: Some(self.info.source.chat.to_string()),
178 from_me: Some(self.info.source.is_from_me),
179 id: Some(self.info.id.clone()),
180 participant: needs_participant.then(|| self.info.source.sender.to_string()),
181 }
182 }
183
184 #[cfg_attr(feature = "tracing", tracing::instrument(name = "wa.bot.edit_message", level = "debug", skip_all, fields(chat = %self.info.source.chat.observe()), err(Debug)))]
185 pub async fn edit_message(
186 &self,
187 original_message_id: impl Into<String>,
188 new_message: wa::Message,
189 ) -> Result<String, crate::send::SendError> {
190 self.client
191 .edit_message(&self.info.source.chat, original_message_id, new_message)
192 .await
193 }
194
195 #[cfg_attr(feature = "tracing", tracing::instrument(name = "wa.bot.revoke_message", level = "debug", skip_all, fields(chat = %self.info.source.chat.observe()), err(Debug)))]
197 pub async fn revoke_message(
198 &self,
199 message_id: impl Into<String>,
200 revoke_type: crate::send::RevokeType,
201 ) -> Result<(), crate::send::SendError> {
202 self.client
203 .revoke_message(&self.info.source.chat, message_id, revoke_type)
204 .await
205 }
206
207 #[cfg_attr(feature = "tracing", tracing::instrument(name = "wa.bot.react", level = "debug", skip_all, fields(chat = %self.info.source.chat.observe()), err(Debug)))]
211 pub async fn react(
212 &self,
213 emoji: &str,
214 ) -> Result<crate::send::SendResult, crate::send::SendError> {
215 self.client
216 .send_reaction(&self.info.source.chat, self.message_key(), emoji)
217 .await
218 }
219}
220
221type EventHandlerCallback =
222 Arc<dyn Fn(Arc<Event>, Arc<Client>) -> Pin<Box<dyn Future<Output = ()> + Send>> + Send + Sync>;
223
224struct RegisteredHandler {
228 callback: EventHandlerCallback,
229 interest: EventInterest,
230}
231
232fn combined_interest(handlers: &[RegisteredHandler]) -> EventInterest {
235 handlers
236 .iter()
237 .fold(EventInterest::none(), |acc, h| acc.union(h.interest))
238}
239
240#[derive(Clone, Copy, Debug, Default)]
242#[non_exhaustive]
243pub enum EventDelivery {
244 #[default]
249 Concurrent,
250 Ordered {
259 capacity: usize,
261 },
262}
263
264enum Delivery {
267 Concurrent { handlers: Arc<[RegisteredHandler]> },
269 Ordered {
271 tx: async_channel::Sender<Arc<Event>>,
272 },
273}
274
275struct CallbackBusAdapter {
276 client: Weak<Client>,
279 delivery: Delivery,
280 interest: EventInterest,
281}
282
283impl CallbackBusAdapter {
284 fn new(client: Arc<Client>, handlers: Vec<RegisteredHandler>, delivery: EventDelivery) -> Self {
285 let interest = combined_interest(&handlers);
286 let delivery = match delivery {
287 EventDelivery::Concurrent => Delivery::Concurrent {
288 handlers: handlers.into(),
289 },
290 EventDelivery::Ordered { capacity } => {
291 let (tx, rx) = async_channel::bounded::<Arc<Event>>(capacity.max(1));
292 let handlers: Arc<[RegisteredHandler]> = handlers.into();
293 let drain_client = Arc::downgrade(&client);
297 let drain_handlers = Arc::clone(&handlers);
298 client
299 .runtime
300 .spawn(Box::pin(async move {
301 while let Ok(event) = rx.recv().await {
302 let Some(client) = drain_client.upgrade() else {
303 break;
304 };
305 let kind = event.kind();
306 for handler in drain_handlers.iter() {
307 if handler.interest.wants(kind) {
308 let cb = handler.callback.clone();
314 let ev = Arc::clone(&event);
315 let cl = client.clone();
316 let ran =
317 std::panic::AssertUnwindSafe(
318 async move { cb(ev, cl).await },
319 )
320 .catch_unwind()
321 .await;
322 if ran.is_err() {
323 warn!(
324 "ordered event delivery callback panicked; continuing"
325 );
326 }
327 }
328 }
329 }
330 }))
331 .detach();
332 Delivery::Ordered { tx }
333 }
334 };
335 Self {
336 client: Arc::downgrade(&client),
337 delivery,
338 interest,
339 }
340 }
341}
342
343impl EventHandler for CallbackBusAdapter {
344 fn handle_event(&self, event: Arc<Event>) {
345 match &self.delivery {
346 Delivery::Concurrent { handlers } => {
347 let Some(client) = self.client.upgrade() else {
348 return;
349 };
350 let kind = event.kind();
351 for handler in handlers.iter() {
352 if !handler.interest.wants(kind) {
353 continue;
354 }
355 let callback = handler.callback.clone();
356 let cb_client = client.clone();
357 let event = Arc::clone(&event);
358 client.runtime.spawn_detached(Box::pin(async move {
359 callback(event, cb_client).await;
360 }));
361 }
362 }
363 Delivery::Ordered { tx } => match tx.try_send(event) {
368 Ok(()) => {}
369 Err(async_channel::TrySendError::Full(_)) => {
370 if let Some(client) = self.client.upgrade() {
371 client.stats.record_event_dropped();
372 }
373 }
374 Err(async_channel::TrySendError::Closed(_)) => {
375 log::debug!("ordered event delivery channel closed; dropping event");
376 }
377 },
378 }
379 }
380
381 fn interest(&self) -> EventInterest {
382 self.interest
383 }
384}
385
386#[must_use = "dropping the handle aborts the bot; bind it and await it, or call .shutdown()"]
392pub struct BotHandle {
393 client: Arc<Client>,
394 done_rx: futures::channel::oneshot::Receiver<()>,
395 abort_handle: wacore::runtime::AbortHandle,
396}
397
398impl BotHandle {
399 pub fn client(&self) -> Arc<Client> {
400 self.client.clone()
401 }
402
403 pub async fn shutdown(mut self) {
407 self.client.disconnect().await;
408 let _ = (&mut self.done_rx).await;
409 }
410
411 pub fn abort(&self) {
415 self.abort_handle.abort();
416 }
417}
418
419impl std::future::Future for BotHandle {
420 type Output = ();
421
422 fn poll(
423 mut self: Pin<&mut Self>,
424 cx: &mut std::task::Context<'_>,
425 ) -> std::task::Poll<Self::Output> {
426 Pin::new(&mut self.done_rx).poll(cx).map(|_| ())
429 }
430}
431
432async fn run_metered<F: std::future::Future<Output = ()>>(
438 fut: F,
439 instrument: Option<Arc<dyn wacore::stats::TaskInstrument>>,
440) {
441 match instrument {
442 Some(i) => wacore::stats::MeteredFuture::new(Box::pin(fut), i).await,
443 None => fut.await,
444 }
445}
446
447pub struct Bot {
483 client: Arc<Client>,
484 sync_task_receiver: Option<async_channel::Receiver<crate::sync_task::MajorSyncTask>>,
485 event_handlers: Vec<RegisteredHandler>,
486 event_delivery: EventDelivery,
487 raw_handlers: Vec<Arc<dyn EventHandler>>,
488 pair_code_options: Option<PairCodeOptions>,
489 task_instrument: Option<Arc<dyn wacore::stats::TaskInstrument>>,
493}
494
495impl std::fmt::Debug for Bot {
496 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
497 f.debug_struct("Bot")
498 .field("client", &"<Client>")
499 .field("sync_task_receiver", &self.sync_task_receiver.is_some())
500 .field("event_handlers", &self.event_handlers.len())
501 .field("event_delivery", &self.event_delivery)
502 .field("raw_handlers", &self.raw_handlers.len())
503 .field("pair_code_options", &self.pair_code_options.is_some())
504 .field("task_instrument", &self.task_instrument.is_some())
505 .finish()
506 }
507}
508
509impl Bot {
510 pub fn builder()
511 -> BotBuilder<MissingBackend, DefaultTransportState, DefaultHttpState, DefaultRuntimeState>
512 {
513 BotBuilder::new()
514 }
515
516 pub fn client(&self) -> Arc<Client> {
517 self.client.clone()
518 }
519
520 pub async fn run(self) {
531 self.run_boxed().await
532 }
533
534 #[inline(never)]
535 fn run_boxed(self) -> wacore::runtime::BoxFuture<'static, ()> {
536 Box::pin(self.run_graph())
537 }
538
539 #[cfg_attr(
540 feature = "tracing",
541 tracing::instrument(name = "wa.bot.run", level = "debug", skip_all)
542 )]
543 async fn run_graph(self) {
544 let instrument = self.task_instrument.clone();
545 let client = self.start_background();
546 run_metered(client.run(), instrument).await;
547 }
548
549 pub fn spawn(self) -> BotHandle {
552 let client = self.start_background();
553
554 let run_client = client.clone();
555 let (done_tx, done_rx) = futures::channel::oneshot::channel::<()>();
556 let abort_handle = client.runtime.spawn(Box::pin(async move {
557 run_client.run().await;
558 let _ = done_tx.send(());
559 }));
560
561 BotHandle {
562 client,
563 done_rx,
564 abort_handle,
565 }
566 }
567
568 fn start_background(self) -> Arc<Client> {
571 let Bot {
572 client,
573 sync_task_receiver,
574 event_handlers,
575 event_delivery,
576 raw_handlers,
577 pair_code_options,
578 task_instrument: _,
579 } = self;
580
581 if let Some(receiver) = sync_task_receiver {
582 client.start_sync_task_worker(receiver);
583 }
584
585 if !event_handlers.is_empty() {
586 client
587 .core
588 .event_bus
589 .subscribe_handler(Arc::new(CallbackBusAdapter::new(
590 client.clone(),
591 event_handlers,
592 event_delivery,
593 )))
594 .detach();
595 }
596 for handler in raw_handlers {
597 client.core.event_bus.subscribe_handler(handler).detach();
598 }
599
600 if let Some(options) = pair_code_options {
602 let client_for_pair = client.clone();
603 client.runtime.spawn(Box::pin(async move {
604 if let Err(e) = client_for_pair
606 .wait_for_socket(std::time::Duration::from_secs(30))
607 .await
608 {
609 warn!(target: "Bot/PairCode", "Timeout waiting for socket: {}", e);
610 client_for_pair.core.event_bus.dispatch(Event::PairingCodeError(
616 crate::types::events::PairingCodeError::builder()
617 .error(e.to_string())
618 .build(),
619 ));
620 return;
621 }
622
623 if client_for_pair.is_logged_in() {
625 info!(target: "Bot/PairCode", "Already logged in, skipping pair code request");
626 return;
627 }
628
629 match client_for_pair.pair_with_code(options).await {
631 Ok(code) => {
632 info!(target: "Bot/PairCode", "Pair code generated: {}", code);
633 }
634 Err(e) => {
635 warn!(target: "Bot/PairCode", "Failed to request pair code: {}", e);
640 }
641 }
642 })).detach();
643 }
644
645 client
646 }
647}
648
649#[must_use = "call .build() to produce the Bot; the builder does nothing on its own"]
658pub struct BotBuilder<
659 B = MissingBackend,
660 T = MissingTransport,
661 H = MissingHttpClient,
662 R = MissingRuntime,
663> {
664 backend: Option<Arc<dyn Backend>>,
666 transport_factory: Option<Arc<dyn crate::transport::TransportFactory>>,
667 http_client: Option<Arc<dyn crate::http::HttpClient>>,
668 runtime: Option<Arc<dyn Runtime>>,
669 event_handlers: Vec<RegisteredHandler>,
671 event_delivery: EventDelivery,
672 raw_handlers: Vec<Arc<dyn EventHandler>>,
673 custom_enc_handlers: HashMap<String, Arc<dyn EncHandler>>,
674 inbound_durability_hook: Option<Arc<dyn InboundDurabilityHook>>,
675 override_version: Option<(u32, u32, u32)>,
676 device_props_override: Option<DevicePropsOverride>,
677 pair_code_options: Option<PairCodeOptions>,
678 skip_history_sync: bool,
679 initial_push_name: Option<String>,
680 cache_config: CacheConfig,
681 wanted_pre_key_count: Option<usize>,
682 resend_rate_limit: Option<(u32, u32)>,
683 task_instrument: Option<Arc<dyn wacore::stats::TaskInstrument>>,
684 alloc_meter: Option<Arc<wacore::stats::AllocMeter>>,
685 #[cfg(feature = "plugins")]
686 plugins: Vec<PluginRegistration>,
687 #[cfg(feature = "plugins")]
688 plugin_host_config: PluginHostConfig,
689 _marker: PhantomData<(B, T, H, R)>,
690}
691
692impl BotBuilder<MissingBackend, DefaultTransportState, DefaultHttpState, DefaultRuntimeState> {
693 fn new() -> Self {
694 Self {
695 backend: None,
696 transport_factory: default_transport_factory(),
697 http_client: default_http_client(),
698 runtime: default_runtime(),
699 event_handlers: Vec::new(),
700 event_delivery: EventDelivery::default(),
701 raw_handlers: Vec::new(),
702 custom_enc_handlers: HashMap::new(),
703 inbound_durability_hook: None,
704 override_version: None,
705 device_props_override: None,
706 pair_code_options: None,
707 skip_history_sync: false,
708 initial_push_name: None,
709 cache_config: CacheConfig::default(),
710 wanted_pre_key_count: None,
711 resend_rate_limit: None,
712 task_instrument: None,
713 alloc_meter: None,
714 #[cfg(feature = "plugins")]
715 plugins: Vec::new(),
716 #[cfg(feature = "plugins")]
717 plugin_host_config: PluginHostConfig::default(),
718 _marker: PhantomData,
719 }
720 }
721}
722
723impl<B, T, H, R> BotBuilder<B, T, H, R> {
724 fn cast<B2, T2, H2, R2>(self) -> BotBuilder<B2, T2, H2, R2> {
727 BotBuilder {
728 backend: self.backend,
729 transport_factory: self.transport_factory,
730 http_client: self.http_client,
731 runtime: self.runtime,
732 event_handlers: self.event_handlers,
733 event_delivery: self.event_delivery,
734 raw_handlers: self.raw_handlers,
735 custom_enc_handlers: self.custom_enc_handlers,
736 inbound_durability_hook: self.inbound_durability_hook,
737 override_version: self.override_version,
738 device_props_override: self.device_props_override,
739 pair_code_options: self.pair_code_options,
740 skip_history_sync: self.skip_history_sync,
741 initial_push_name: self.initial_push_name,
742 cache_config: self.cache_config,
743 wanted_pre_key_count: self.wanted_pre_key_count,
744 resend_rate_limit: self.resend_rate_limit,
745 task_instrument: self.task_instrument,
746 alloc_meter: self.alloc_meter,
747 #[cfg(feature = "plugins")]
748 plugins: self.plugins,
749 #[cfg(feature = "plugins")]
750 plugin_host_config: self.plugin_host_config,
751 _marker: PhantomData,
752 }
753 }
754
755 pub fn with_backend(self, backend: impl Backend + 'static) -> BotBuilder<Provided, T, H, R> {
771 self.with_backend_arc(Arc::new(backend))
772 }
773
774 pub fn with_backend_arc(mut self, backend: Arc<dyn Backend>) -> BotBuilder<Provided, T, H, R> {
776 self.backend = Some(backend);
777 self.cast()
778 }
779
780 pub fn with_transport_factory<F>(mut self, factory: F) -> BotBuilder<B, Provided, H, R>
783 where
784 F: crate::transport::TransportFactory + 'static,
785 {
786 self.transport_factory = Some(Arc::new(factory));
787 self.cast()
788 }
789
790 pub fn with_http_client<C>(mut self, client: C) -> BotBuilder<B, T, Provided, R>
793 where
794 C: crate::http::HttpClient + 'static,
795 {
796 self.http_client = Some(Arc::new(client));
797 self.cast()
798 }
799
800 pub fn with_runtime<Rt: Runtime>(mut self, runtime: Rt) -> BotBuilder<B, T, H, Provided> {
803 self.runtime = Some(Arc::new(runtime));
804 self.cast()
805 }
806
807 pub fn with_task_instrument(
841 mut self,
842 instrument: Arc<dyn wacore::stats::TaskInstrument>,
843 ) -> Self {
844 self.task_instrument = Some(instrument);
845 self.alloc_meter = None;
849 self
850 }
851
852 pub fn with_alloc_meter(mut self, meter: Arc<wacore::stats::AllocMeter>) -> Self {
865 self.task_instrument = Some(meter.clone());
866 self.alloc_meter = Some(meter);
867 self
868 }
869
870 #[cfg(feature = "plugins")]
872 #[cfg_attr(docsrs, doc(cfg(feature = "plugins")))]
873 pub fn with_plugin<P: ClientPlugin>(mut self, plugin: P) -> Self {
874 self.plugins.push(PluginRegistration::new(plugin));
875 self
876 }
877
878 #[cfg(feature = "plugins")]
880 #[cfg_attr(docsrs, doc(cfg(feature = "plugins")))]
881 pub fn with_plugin_arc<P: ClientPlugin>(mut self, plugin: Arc<P>) -> Self {
882 self.plugins.push(PluginRegistration::new_arc(plugin));
883 self
884 }
885
886 #[cfg(feature = "plugins")]
888 #[cfg_attr(docsrs, doc(cfg(feature = "plugins")))]
889 pub fn with_untyped_plugin<P: UntypedClientPlugin>(mut self, plugin: P) -> Self {
890 self.plugins.push(PluginRegistration::new_untyped(plugin));
891 self
892 }
893
894 #[cfg(feature = "plugins")]
896 #[cfg_attr(docsrs, doc(cfg(feature = "plugins")))]
897 pub fn with_untyped_plugin_arc<P: UntypedClientPlugin + ?Sized>(
898 mut self,
899 plugin: Arc<P>,
900 ) -> Self {
901 self.plugins
902 .push(PluginRegistration::new_untyped_arc(plugin));
903 self
904 }
905
906 #[cfg(feature = "plugins")]
908 #[cfg_attr(docsrs, doc(cfg(feature = "plugins")))]
909 pub fn with_plugin_host_config(mut self, config: PluginHostConfig) -> Self {
910 self.plugin_host_config = config;
911 self
912 }
913
914 pub fn on_event<F, Fut>(self, handler: F) -> Self
920 where
921 F: Fn(Arc<Event>, Arc<Client>) -> Fut + Send + Sync + 'static,
922 Fut: Future<Output = ()> + Send + 'static,
923 {
924 self.register_event_handler(EventInterest::ALL, handler)
925 }
926
927 pub fn on_event_for<F, Fut>(self, kinds: &[EventKind], handler: F) -> Self
931 where
932 F: Fn(Arc<Event>, Arc<Client>) -> Fut + Send + Sync + 'static,
933 Fut: Future<Output = ()> + Send + 'static,
934 {
935 self.register_event_handler(EventInterest::of(kinds), handler)
936 }
937
938 pub fn on_message<F, Fut>(self, handler: F) -> Self
952 where
953 F: Fn(MessageContext) -> Fut + Send + Sync + 'static,
954 Fut: Future<Output = ()> + Send + 'static,
955 {
956 self.on_event_for(&[EventKind::Messages], move |event, client| {
957 let futures: Vec<Fut> = event
962 .messages()
963 .map(|m| handler(MessageContext::from_inbound(m, Arc::clone(&client))))
964 .collect();
965 async move {
966 for future in futures {
967 future.await;
968 }
969 }
970 })
971 }
972
973 pub fn on_qr_code<F, Fut>(self, handler: F) -> Self
976 where
977 F: Fn(String, std::time::Duration) -> Fut + Send + Sync + 'static,
978 Fut: Future<Output = ()> + Send + 'static,
979 {
980 self.on_event_for(&[EventKind::PairingQrCode], move |event, _client| {
981 let fut = match &*event {
982 Event::PairingQrCode(qr) => Some(handler(qr.code.clone(), qr.timeout)),
983 _ => None,
984 };
985 async move {
986 if let Some(fut) = fut {
987 fut.await
988 }
989 }
990 })
991 }
992
993 pub fn on_pair_code<F, Fut>(self, handler: F) -> Self
996 where
997 F: Fn(String, std::time::Duration) -> Fut + Send + Sync + 'static,
998 Fut: Future<Output = ()> + Send + 'static,
999 {
1000 self.on_event_for(&[EventKind::PairingCode], move |event, _client| {
1001 let fut = match &*event {
1002 Event::PairingCode(pc) => Some(handler(pc.code.clone(), pc.timeout)),
1003 _ => None,
1004 };
1005 async move {
1006 if let Some(fut) = fut {
1007 fut.await
1008 }
1009 }
1010 })
1011 }
1012
1013 pub fn on_pair_code_error<F, Fut>(self, handler: F) -> Self
1026 where
1027 F: Fn(crate::types::events::PairingCodeError, Arc<Client>) -> Fut + Send + Sync + 'static,
1028 Fut: Future<Output = ()> + Send + 'static,
1029 {
1030 self.on_event_for(&[EventKind::PairingCodeError], move |event, client| {
1031 let fut = match &*event {
1032 Event::PairingCodeError(e) => Some(handler(e.clone(), client)),
1033 _ => None,
1034 };
1035 async move {
1036 if let Some(fut) = fut {
1037 fut.await
1038 }
1039 }
1040 })
1041 }
1042
1043 pub fn on_pair_code_refresh<F, Fut>(self, handler: F) -> Self
1048 where
1049 F: Fn(bool, Arc<Client>) -> Fut + Send + Sync + 'static,
1050 Fut: Future<Output = ()> + Send + 'static,
1051 {
1052 self.on_event_for(&[EventKind::PairingCodeRefresh], move |event, client| {
1053 let fut = match &*event {
1054 Event::PairingCodeRefresh(r) => Some(handler(r.force_manual, client)),
1055 _ => None,
1056 };
1057 async move {
1058 if let Some(fut) = fut {
1059 fut.await
1060 }
1061 }
1062 })
1063 }
1064
1065 pub fn on_connected<F, Fut>(self, handler: F) -> Self
1067 where
1068 F: Fn(Arc<Client>) -> Fut + Send + Sync + 'static,
1069 Fut: Future<Output = ()> + Send + 'static,
1070 {
1071 self.on_event_for(&[EventKind::Connected], move |_event, client| {
1072 handler(client)
1073 })
1074 }
1075
1076 pub fn on_logged_out<F, Fut>(self, handler: F) -> Self
1078 where
1079 F: Fn(crate::types::events::LoggedOut) -> Fut + Send + Sync + 'static,
1080 Fut: Future<Output = ()> + Send + 'static,
1081 {
1082 self.on_event_for(&[EventKind::LoggedOut], move |event, _client| {
1083 let fut = match &*event {
1084 Event::LoggedOut(info) => Some(handler(info.clone())),
1085 _ => None,
1086 };
1087 async move {
1088 if let Some(fut) = fut {
1089 fut.await
1090 }
1091 }
1092 })
1093 }
1094
1095 fn register_event_handler<F, Fut>(mut self, interest: EventInterest, handler: F) -> Self
1096 where
1097 F: Fn(Arc<Event>, Arc<Client>) -> Fut + Send + Sync + 'static,
1098 Fut: Future<Output = ()> + Send + 'static,
1099 {
1100 self.event_handlers.push(RegisteredHandler {
1101 callback: Arc::new(move |event, client| Box::pin(handler(event, client))),
1102 interest,
1103 });
1104 self
1105 }
1106
1107 pub fn with_event_handler(mut self, handler: impl EventHandler + 'static) -> Self {
1113 self.raw_handlers.push(Arc::new(handler));
1114 self
1115 }
1116
1117 pub fn with_event_delivery(mut self, delivery: EventDelivery) -> Self {
1124 self.event_delivery = delivery;
1125 self
1126 }
1127
1128 pub fn with_enc_handler<Eh>(mut self, enc_type: impl Into<String>, handler: Eh) -> Self
1136 where
1137 Eh: EncHandler + 'static,
1138 {
1139 self.custom_enc_handlers
1140 .insert(enc_type.into(), Arc::new(handler));
1141 self
1142 }
1143
1144 pub fn with_inbound_durability_hook<Dh>(mut self, hook: Dh) -> Self
1154 where
1155 Dh: InboundDurabilityHook + 'static,
1156 {
1157 self.inbound_durability_hook = Some(Arc::new(hook));
1158 self
1159 }
1160
1161 pub fn with_version(mut self, version: (u32, u32, u32)) -> Self {
1169 self.override_version = Some(version);
1170 self
1171 }
1172
1173 pub fn with_device_props(mut self, override_: DevicePropsOverride) -> Self {
1193 self.device_props_override = Some(override_);
1194 self
1195 }
1196
1197 pub fn with_pair_code(mut self, options: PairCodeOptions) -> Self {
1231 self.pair_code_options = Some(options);
1232 self
1233 }
1234
1235 pub fn skip_history_sync(mut self) -> Self {
1246 self.skip_history_sync = true;
1247 self
1248 }
1249
1250 pub fn with_wanted_pre_key_count(mut self, count: usize) -> Self {
1256 self.wanted_pre_key_count = Some(count);
1257 self
1258 }
1259
1260 pub fn with_resend_rate_limit(mut self, burst: u32, refill_per_min: u32) -> Self {
1272 self.resend_rate_limit = Some((burst, refill_per_min));
1273 self
1274 }
1275
1276 pub fn with_push_name(mut self, name: impl Into<String>) -> Self {
1282 self.initial_push_name = Some(name.into());
1283 self
1284 }
1285
1286 pub fn with_cache_config(mut self, config: CacheConfig) -> Self {
1307 self.cache_config = config;
1308 self
1309 }
1310}
1311
1312impl BotBuilder<Provided, Provided, Provided, Provided> {
1315 pub async fn build(self) -> Result<Bot, BotBuilderError> {
1319 self.build_boxed().await
1320 }
1321
1322 #[inline(never)]
1323 fn build_boxed(self) -> wacore::runtime::BoxFuture<'static, Result<Bot, BotBuilderError>> {
1324 Box::pin(self.build_graph())
1325 }
1326
1327 #[cfg_attr(
1328 feature = "tracing",
1329 tracing::instrument(name = "wa.bot.build", level = "debug", skip_all, err(Debug))
1330 )]
1331 async fn build_graph(self) -> Result<Bot, BotBuilderError> {
1332 let (Some(runtime), Some(backend), Some(transport_factory), Some(http_client)) = (
1334 self.runtime,
1335 self.backend,
1336 self.transport_factory,
1337 self.http_client,
1338 ) else {
1339 unreachable!("typestate guarantees all required fields are Provided")
1340 };
1341
1342 let task_instrument = self.task_instrument;
1343 let alloc_meter = self.alloc_meter;
1344
1345 let persistence_manager = Arc::new(PersistenceManager::new(backend).await?);
1348
1349 if let Some(name) = self.initial_push_name {
1351 persistence_manager
1352 .process_command(DeviceCommand::SetPushName(name))
1353 .await;
1354 }
1355
1356 if let Some(override_) = self.device_props_override
1357 && !override_.is_empty()
1358 {
1359 info!(
1362 "Applying device props override: os={:?} version={:?} platform_type={:?} require_full_sync={:?} history_sync_config={}",
1363 override_.os.as_deref(),
1364 override_.version.as_ref().map(|v| {
1365 format!(
1366 "{}.{}.{}",
1367 v.primary.unwrap_or(0),
1368 v.secondary.unwrap_or(0),
1369 v.tertiary.unwrap_or(0)
1370 )
1371 }),
1372 override_.platform_type.map(|p| p as i32),
1373 override_.require_full_sync,
1374 if override_.history_sync_config.is_some() {
1375 "overridden"
1376 } else {
1377 "default"
1378 },
1379 );
1380 persistence_manager
1381 .process_command(DeviceCommand::SetDeviceProps(override_))
1382 .await;
1383 }
1384
1385 info!("Creating client...");
1386 let client_builder = Client::builder()
1387 .with_runtime_arc(runtime)
1388 .with_persistence_manager(persistence_manager)
1389 .with_transport_factory_arc(transport_factory)
1390 .with_http_client_arc(http_client)
1391 .with_cache_config(self.cache_config)
1392 .with_custom_enc_handlers(self.custom_enc_handlers)
1393 .with_skip_history_sync(self.skip_history_sync)
1394 .with_background_saver_interval(std::time::Duration::from_secs(30));
1395 #[cfg(feature = "plugins")]
1396 let client_builder = client_builder
1397 .with_plugin_registrations(self.plugins)
1398 .with_plugin_host_config(self.plugin_host_config);
1399 let mut client_builder = client_builder;
1400
1401 if let Some(version) = self.override_version {
1402 client_builder = client_builder.with_version_override(version);
1403 }
1404 if let Some(hook) = self.inbound_durability_hook {
1405 client_builder = client_builder.with_inbound_durability_hook_arc(hook);
1406 }
1407 if let Some(count) = self.wanted_pre_key_count {
1408 client_builder = client_builder.with_wanted_pre_key_count(count);
1409 }
1410 if let Some((burst, refill_per_min)) = self.resend_rate_limit {
1411 client_builder = client_builder.with_resend_rate_limit(burst, refill_per_min);
1412 }
1413 client_builder = match alloc_meter {
1414 Some(meter) => client_builder.with_alloc_meter(meter),
1415 None => match task_instrument.clone() {
1416 Some(instrument) => client_builder.with_task_instrument(instrument),
1417 None => client_builder,
1418 },
1419 };
1420
1421 let (client, sync_task_receiver) = client_builder.build().await?.into_parts();
1422
1423 Ok(Bot {
1424 client,
1425 sync_task_receiver: Some(sync_task_receiver),
1426 event_handlers: self.event_handlers,
1427 event_delivery: self.event_delivery,
1428 raw_handlers: self.raw_handlers,
1429 pair_code_options: self.pair_code_options,
1430 task_instrument,
1431 })
1432 }
1433}
1434
1435#[cfg(test)]
1436mod tests {
1437 use super::*;
1438 use crate::TokioRuntime;
1439 use crate::http::{HttpClient, HttpRequest, HttpResponse};
1440 use crate::store::SqliteStore;
1441 use anyhow::Result;
1442 use whatsapp_rust_tokio_transport::TokioWebSocketTransportFactory;
1443
1444 #[derive(Debug, Clone)]
1446 struct MockHttpClient;
1447
1448 #[async_trait::async_trait]
1449 impl HttpClient for MockHttpClient {
1450 async fn execute(&self, _request: HttpRequest) -> Result<HttpResponse> {
1451 Ok(HttpResponse {
1453 status_code: 200,
1454 body: br#"self.__swData=JSON.parse(/*BTDS*/"{\"dynamic_data\":{\"SiteData\":{\"server_revision\":1026131876,\"client_revision\":1026131876}}}");"#.to_vec(),
1455 })
1456 }
1457 }
1458
1459 async fn create_test_sqlite_backend() -> Arc<dyn Backend> {
1460 let temp_db = format!(
1461 "file:memdb_bot_{}?mode=memory&cache=shared",
1462 uuid::Uuid::new_v4()
1463 );
1464 Arc::new(
1465 SqliteStore::new(&temp_db)
1466 .await
1467 .expect("Failed to create test SqliteStore"),
1468 ) as Arc<dyn Backend>
1469 }
1470
1471 async fn create_test_sqlite_backend_for_device(device_id: i32) -> Arc<dyn Backend> {
1472 let temp_db = format!(
1473 "file:memdb_bot_{}?mode=memory&cache=shared",
1474 uuid::Uuid::new_v4()
1475 );
1476 Arc::new(
1477 SqliteStore::new_for_device(&temp_db, device_id)
1478 .await
1479 .expect("Failed to create test SqliteStore"),
1480 ) as Arc<dyn Backend>
1481 }
1482
1483 async fn test_client() -> Arc<Client> {
1484 Bot::builder()
1485 .with_backend_arc(create_test_sqlite_backend().await)
1486 .with_transport_factory(TokioWebSocketTransportFactory::new())
1487 .with_http_client(MockHttpClient)
1488 .with_runtime(TokioRuntime)
1489 .build()
1490 .await
1491 .expect("build")
1492 .client()
1493 }
1494
1495 #[cfg(feature = "plugins")]
1496 struct BotBuilderPlugin;
1497
1498 #[cfg(feature = "plugins")]
1499 impl ClientPlugin for BotBuilderPlugin {
1500 type Api = &'static str;
1501
1502 fn manifest(&self) -> crate::plugins::PluginManifest {
1503 crate::plugins::PluginManifest::new("bot-builder-test", "0.1.0")
1504 }
1505
1506 fn install(
1507 &self,
1508 _context: crate::plugins::PluginContext,
1509 ) -> wacore::runtime::BoxFuture<'_, Result<Arc<Self::Api>>> {
1510 Box::pin(async { Ok(Arc::new("installed")) })
1511 }
1512 }
1513
1514 #[cfg(feature = "plugins")]
1515 #[tokio::test]
1516 async fn typestate_builder_preserves_registered_plugins() {
1517 let bot = Bot::builder()
1518 .with_plugin(BotBuilderPlugin)
1519 .with_backend_arc(create_test_sqlite_backend().await)
1520 .with_transport_factory(TokioWebSocketTransportFactory::new())
1521 .with_http_client(MockHttpClient)
1522 .with_runtime(TokioRuntime)
1523 .build()
1524 .await
1525 .expect("bot plugin build");
1526 assert_eq!(
1527 bot.client().plugin::<BotBuilderPlugin>().as_deref(),
1528 Some(&"installed")
1529 );
1530 bot.client().disconnect().await;
1531 }
1532
1533 fn pairing_code_event(code: &str) -> Arc<Event> {
1534 Arc::new(Event::PairingCode(
1535 crate::types::events::PairingCode::builder()
1536 .code(code.to_string())
1537 .timeout(std::time::Duration::ZERO)
1538 .build(),
1539 ))
1540 }
1541
1542 #[tokio::test]
1545 async fn ordered_delivery_preserves_arrival_order() {
1546 let client = test_client().await;
1547 let (order_tx, order_rx) = async_channel::unbounded::<String>();
1548 let handler = RegisteredHandler {
1549 callback: Arc::new(move |event, _client| {
1550 let order_tx = order_tx.clone();
1551 Box::pin(async move {
1552 if let Event::PairingCode(pc) = &*event {
1553 let _ = order_tx.send(pc.code.clone()).await;
1554 }
1555 })
1556 }),
1557 interest: EventInterest::ALL,
1558 };
1559 let adapter = CallbackBusAdapter::new(
1560 client.clone(),
1561 vec![handler],
1562 EventDelivery::Ordered { capacity: 64 },
1563 );
1564
1565 let n = 20;
1566 for i in 0..n {
1567 adapter.handle_event(pairing_code_event(&i.to_string()));
1568 }
1569
1570 let mut seen = Vec::with_capacity(n);
1571 for _ in 0..n {
1572 seen.push(
1573 tokio::time::timeout(std::time::Duration::from_secs(5), order_rx.recv())
1574 .await
1575 .expect("timed out waiting for ordered callback")
1576 .expect("callback ran"),
1577 );
1578 }
1579 let expected: Vec<String> = (0..n).map(|i| i.to_string()).collect();
1580 assert_eq!(
1581 seen, expected,
1582 "ordered delivery must preserve arrival order"
1583 );
1584 }
1585
1586 #[tokio::test]
1589 async fn ordered_delivery_drops_and_counts_when_full() {
1590 let client = test_client().await;
1591 let (started_tx, started_rx) = async_channel::bounded::<()>(1);
1592 let (release_tx, release_rx) = async_channel::bounded::<()>(1);
1593 let handler = RegisteredHandler {
1594 callback: Arc::new(move |event, _client| {
1595 let started_tx = started_tx.clone();
1596 let release_rx = release_rx.clone();
1597 Box::pin(async move {
1598 if let Event::PairingCode(pc) = &*event
1601 && pc.code == "0"
1602 {
1603 let _ = started_tx.send(()).await;
1604 let _ = release_rx.recv().await;
1605 }
1606 })
1607 }),
1608 interest: EventInterest::ALL,
1609 };
1610 let adapter = CallbackBusAdapter::new(
1611 client.clone(),
1612 vec![handler],
1613 EventDelivery::Ordered { capacity: 1 },
1614 );
1615
1616 adapter.handle_event(pairing_code_event("0"));
1619 tokio::time::timeout(std::time::Duration::from_secs(5), started_rx.recv())
1620 .await
1621 .expect("timed out waiting for drainer to start")
1622 .expect("drainer entered callback");
1623
1624 adapter.handle_event(pairing_code_event("1")); adapter.handle_event(pairing_code_event("2")); adapter.handle_event(pairing_code_event("3")); assert_eq!(
1629 client.stats.events_dropped(),
1630 2,
1631 "events past a full mailbox must be dropped and counted"
1632 );
1633
1634 let _ = release_tx.send(()).await; }
1636
1637 #[tokio::test]
1641 async fn ordered_delivery_survives_a_panicking_callback() {
1642 let client = test_client().await;
1643 let (tx, rx) = async_channel::unbounded::<String>();
1644 let handler = RegisteredHandler {
1645 callback: Arc::new(move |event: Arc<Event>, _client| {
1646 let tx = tx.clone();
1647 if let Event::PairingCode(pc) = &*event {
1648 assert_ne!(&pc.code, "boom", "deliberate test panic");
1649 }
1650 Box::pin(async move {
1651 if let Event::PairingCode(pc) = &*event {
1652 let _ = tx.send(pc.code.clone()).await;
1653 }
1654 })
1655 }),
1656 interest: EventInterest::ALL,
1657 };
1658 let adapter = CallbackBusAdapter::new(
1659 client.clone(),
1660 vec![handler],
1661 EventDelivery::Ordered { capacity: 16 },
1662 );
1663
1664 adapter.handle_event(pairing_code_event("a"));
1665 adapter.handle_event(pairing_code_event("boom")); adapter.handle_event(pairing_code_event("b"));
1667
1668 let mut seen = Vec::with_capacity(2);
1669 for _ in 0..2 {
1670 seen.push(
1671 tokio::time::timeout(std::time::Duration::from_secs(5), rx.recv())
1672 .await
1673 .expect("timed out waiting for post-panic delivery")
1674 .expect("callback ran"),
1675 );
1676 }
1677 assert_eq!(
1678 seen,
1679 vec!["a".to_string(), "b".to_string()],
1680 "delivery must continue after a callback panic"
1681 );
1682 }
1683
1684 #[tokio::test]
1688 async fn task_instrument_after_alloc_meter_clears_stale_handle() {
1689 use wacore::stats::{AllocMeter, CpuMeter};
1690
1691 let bot = Bot::builder()
1692 .with_backend_arc(create_test_sqlite_backend().await)
1693 .with_transport_factory(TokioWebSocketTransportFactory::new())
1694 .with_http_client(MockHttpClient)
1695 .with_runtime(TokioRuntime)
1696 .with_alloc_meter(Arc::new(AllocMeter::new()))
1697 .with_task_instrument(Arc::new(CpuMeter::new()))
1698 .build()
1699 .await
1700 .expect("build");
1701 assert!(
1702 bot.client().resource_report().await.alloc.is_none(),
1703 "a later with_task_instrument must drop the alloc-meter handle"
1704 );
1705
1706 let bot = Bot::builder()
1708 .with_backend_arc(create_test_sqlite_backend().await)
1709 .with_transport_factory(TokioWebSocketTransportFactory::new())
1710 .with_http_client(MockHttpClient)
1711 .with_runtime(TokioRuntime)
1712 .with_task_instrument(Arc::new(CpuMeter::new()))
1713 .with_alloc_meter(Arc::new(AllocMeter::new()))
1714 .build()
1715 .await
1716 .expect("build");
1717 assert!(
1718 bot.client().resource_report().await.alloc.is_some(),
1719 "with_alloc_meter installs the handle when it is the last setter"
1720 );
1721 }
1722
1723 #[tokio::test]
1724 async fn test_bot_builder_single_device() {
1725 let backend = create_test_sqlite_backend().await;
1726 let transport = TokioWebSocketTransportFactory::new();
1727 let http_client = MockHttpClient;
1728
1729 let bot = Bot::builder()
1730 .with_backend_arc(backend)
1731 .with_transport_factory(transport)
1732 .with_http_client(http_client)
1733 .with_runtime(TokioRuntime)
1734 .build()
1735 .await
1736 .expect("Failed to build bot");
1737
1738 let _client = bot.client();
1740 }
1741
1742 #[tokio::test]
1743 async fn test_bot_builder_multi_device() {
1744 let backend = create_test_sqlite_backend_for_device(42).await;
1746 let transport = TokioWebSocketTransportFactory::new();
1747
1748 let bot = Bot::builder()
1749 .with_backend_arc(backend)
1750 .with_transport_factory(transport)
1751 .with_http_client(MockHttpClient)
1752 .with_runtime(TokioRuntime)
1753 .build()
1754 .await
1755 .expect("Failed to build bot");
1756
1757 let _client = bot.client();
1759 }
1760
1761 #[tokio::test]
1762 async fn test_bot_builder_defaults_only_need_backend() {
1763 let temp_db = format!(
1766 "file:memdb_bot_{}?mode=memory&cache=shared",
1767 uuid::Uuid::new_v4()
1768 );
1769 let store = SqliteStore::new(&temp_db)
1770 .await
1771 .expect("Failed to create test SqliteStore");
1772
1773 let bot = Bot::builder()
1774 .with_backend(store)
1775 .with_http_client(MockHttpClient)
1777 .build()
1778 .await
1779 .expect("Failed to build bot from defaults");
1780
1781 let _client = bot.client();
1782 }
1783
1784 #[tokio::test]
1785 async fn test_bot_builder_with_version_override() {
1786 let backend = create_test_sqlite_backend().await;
1787 let transport = TokioWebSocketTransportFactory::new();
1788 let http_client = MockHttpClient;
1789
1790 let bot = Bot::builder()
1791 .with_backend_arc(backend)
1792 .with_transport_factory(transport)
1793 .with_http_client(http_client)
1794 .with_version((2, 3000, 123456789))
1795 .with_runtime(TokioRuntime)
1796 .build()
1797 .await
1798 .expect("Failed to build bot with version override");
1799
1800 let client = bot.client();
1802
1803 assert_eq!(client.override_version, Some((2, 3000, 123456789)));
1805 }
1806
1807 #[tokio::test]
1808 async fn test_bot_builder_with_device_props_override() {
1809 let backend = create_test_sqlite_backend().await;
1810 let transport = TokioWebSocketTransportFactory::new();
1811 let http_client = MockHttpClient;
1812
1813 let custom_os = "CustomOS".to_string();
1814 let custom_version = wa::device_props::AppVersion {
1815 primary: Some(99),
1816 secondary: Some(88),
1817 tertiary: Some(77),
1818 ..Default::default()
1819 };
1820
1821 let bot = Bot::builder()
1822 .with_backend_arc(backend)
1823 .with_transport_factory(transport)
1824 .with_http_client(http_client)
1825 .with_device_props(
1826 DevicePropsOverride::new()
1827 .with_os(custom_os.clone())
1828 .with_version(custom_version.clone()),
1829 )
1830 .with_runtime(TokioRuntime)
1831 .build()
1832 .await
1833 .expect("Failed to build bot with device props override");
1834
1835 let client = bot.client();
1836 let persistence_manager = client.persistence_manager();
1837 let device = persistence_manager.get_device_snapshot();
1838
1839 assert_eq!(device.device_props.os, Some(custom_os));
1841 assert_eq!(
1842 device.device_props.version.as_option(),
1843 Some(&custom_version)
1844 );
1845 }
1846
1847 #[tokio::test]
1848 async fn test_bot_builder_with_os_only_override() {
1849 let backend = create_test_sqlite_backend().await;
1850 let transport = TokioWebSocketTransportFactory::new();
1851 let http_client = MockHttpClient;
1852
1853 let custom_os = "CustomOS".to_string();
1854
1855 let bot = Bot::builder()
1856 .with_backend_arc(backend)
1857 .with_transport_factory(transport)
1858 .with_http_client(http_client)
1859 .with_device_props(DevicePropsOverride::new().with_os(custom_os.clone()))
1860 .with_runtime(TokioRuntime)
1861 .build()
1862 .await
1863 .expect("Failed to build bot with OS only override");
1864
1865 let client = bot.client();
1866 let persistence_manager = client.persistence_manager();
1867 let device = persistence_manager.get_device_snapshot();
1868
1869 assert_eq!(device.device_props.os, Some(custom_os));
1871 assert_eq!(
1873 device.device_props.version.as_option(),
1874 Some(&wacore::store::Device::default_device_props_version())
1875 );
1876 }
1877
1878 #[tokio::test]
1879 async fn test_bot_builder_with_version_only_override() {
1880 let backend = create_test_sqlite_backend().await;
1881 let transport = TokioWebSocketTransportFactory::new();
1882 let http_client = MockHttpClient;
1883
1884 let custom_version = wa::device_props::AppVersion {
1885 primary: Some(99),
1886 secondary: Some(88),
1887 tertiary: Some(77),
1888 ..Default::default()
1889 };
1890
1891 let bot = Bot::builder()
1892 .with_backend_arc(backend)
1893 .with_http_client(http_client)
1894 .with_transport_factory(transport)
1895 .with_device_props(DevicePropsOverride::new().with_version(custom_version.clone()))
1896 .with_runtime(TokioRuntime)
1897 .build()
1898 .await
1899 .expect("Failed to build bot with version only override");
1900
1901 let client = bot.client();
1902 let persistence_manager = client.persistence_manager();
1903 let device = persistence_manager.get_device_snapshot();
1904
1905 assert_eq!(
1907 device.device_props.version.as_option(),
1908 Some(&custom_version)
1909 );
1910 assert_eq!(
1912 device.device_props.os,
1913 Some(wacore::store::Device::default_os().to_string())
1914 );
1915 }
1916
1917 #[tokio::test]
1918 async fn test_bot_builder_with_platform_type_override() {
1919 let backend = create_test_sqlite_backend().await;
1920 let transport = TokioWebSocketTransportFactory::new();
1921 let http_client = MockHttpClient;
1922
1923 let bot = Bot::builder()
1924 .with_backend_arc(backend)
1925 .with_transport_factory(transport)
1926 .with_http_client(http_client)
1927 .with_device_props(
1928 DevicePropsOverride::new()
1929 .with_platform_type(wa::device_props::PlatformType::CHROME),
1930 )
1931 .with_runtime(TokioRuntime)
1932 .build()
1933 .await
1934 .expect("Failed to build bot with platform type override");
1935
1936 let client = bot.client();
1937 let persistence_manager = client.persistence_manager();
1938 let device = persistence_manager.get_device_snapshot();
1939
1940 assert_eq!(
1942 device.device_props.platform_type,
1943 Some(wa::device_props::PlatformType::CHROME)
1944 );
1945 assert_eq!(
1947 device.device_props.os,
1948 Some(wacore::store::Device::default_os().to_string())
1949 );
1950 assert_eq!(
1951 device.device_props.version.as_option(),
1952 Some(&wacore::store::Device::default_device_props_version())
1953 );
1954 }
1955
1956 #[tokio::test]
1957 async fn test_bot_builder_with_full_device_props_override() {
1958 let backend = create_test_sqlite_backend().await;
1959 let transport = TokioWebSocketTransportFactory::new();
1960 let http_client = MockHttpClient;
1961
1962 let custom_os = "macOS".to_string();
1963 let custom_version = wa::device_props::AppVersion {
1964 primary: Some(2),
1965 secondary: Some(0),
1966 tertiary: Some(0),
1967 ..Default::default()
1968 };
1969 let custom_platform = wa::device_props::PlatformType::SAFARI;
1970
1971 let bot = Bot::builder()
1972 .with_backend_arc(backend)
1973 .with_transport_factory(transport)
1974 .with_http_client(http_client)
1975 .with_device_props(
1976 DevicePropsOverride::new()
1977 .with_os(custom_os.clone())
1978 .with_version(custom_version.clone())
1979 .with_platform_type(custom_platform),
1980 )
1981 .with_runtime(TokioRuntime)
1982 .build()
1983 .await
1984 .expect("Failed to build bot with full device props override");
1985
1986 let client = bot.client();
1987 let persistence_manager = client.persistence_manager();
1988 let device = persistence_manager.get_device_snapshot();
1989
1990 assert_eq!(device.device_props.os, Some(custom_os));
1992 assert_eq!(
1993 device.device_props.version.as_option(),
1994 Some(&custom_version)
1995 );
1996 assert_eq!(device.device_props.platform_type, Some(custom_platform));
1997 }
1998
1999 #[tokio::test]
2000 async fn test_bot_builder_skip_history_sync() {
2001 let backend = create_test_sqlite_backend().await;
2002 let transport = TokioWebSocketTransportFactory::new();
2003 let http_client = MockHttpClient;
2004
2005 let bot = Bot::builder()
2006 .with_backend_arc(backend)
2007 .with_transport_factory(transport)
2008 .with_http_client(http_client)
2009 .skip_history_sync()
2010 .with_runtime(TokioRuntime)
2011 .build()
2012 .await
2013 .expect("Failed to build bot with skip_history_sync");
2014
2015 assert!(bot.client().skip_history_sync_enabled());
2016 }
2017
2018 #[tokio::test]
2019 async fn test_bot_builder_default_history_sync_enabled() {
2020 let backend = create_test_sqlite_backend().await;
2021 let transport = TokioWebSocketTransportFactory::new();
2022 let http_client = MockHttpClient;
2023
2024 let bot = Bot::builder()
2025 .with_backend_arc(backend)
2026 .with_transport_factory(transport)
2027 .with_http_client(http_client)
2028 .with_runtime(TokioRuntime)
2029 .build()
2030 .await
2031 .expect("Failed to build bot");
2032
2033 assert!(!bot.client().skip_history_sync_enabled());
2034 }
2035
2036 #[tokio::test]
2037 async fn test_bot_builder_wanted_pre_key_count() {
2038 let backend = create_test_sqlite_backend().await;
2039 let transport = TokioWebSocketTransportFactory::new();
2040 let http_client = MockHttpClient;
2041
2042 let bot = Bot::builder()
2043 .with_backend_arc(backend)
2044 .with_transport_factory(transport)
2045 .with_http_client(http_client)
2046 .with_wanted_pre_key_count(200)
2047 .with_runtime(TokioRuntime)
2048 .build()
2049 .await
2050 .expect("Failed to build bot with custom pre-key count");
2051
2052 assert_eq!(bot.client().wanted_pre_key_count(), 200);
2053 }
2054
2055 #[tokio::test]
2056 async fn test_bot_builder_default_wanted_pre_key_count() {
2057 let backend = create_test_sqlite_backend().await;
2058 let transport = TokioWebSocketTransportFactory::new();
2059 let http_client = MockHttpClient;
2060
2061 let bot = Bot::builder()
2062 .with_backend_arc(backend)
2063 .with_transport_factory(transport)
2064 .with_http_client(http_client)
2065 .with_runtime(TokioRuntime)
2066 .build()
2067 .await
2068 .expect("Failed to build bot");
2069
2070 assert_eq!(
2071 bot.client().wanted_pre_key_count(),
2072 crate::prekeys::DEFAULT_WANTED_PRE_KEY_COUNT
2073 );
2074 }
2075
2076 #[tokio::test]
2077 async fn registered_handlers_accumulate_instead_of_replacing() {
2078 let backend = create_test_sqlite_backend().await;
2079
2080 let bot = Bot::builder()
2081 .with_backend_arc(backend)
2082 .with_transport_factory(TokioWebSocketTransportFactory::new())
2083 .with_http_client(MockHttpClient)
2084 .with_runtime(TokioRuntime)
2085 .on_message(|_ctx| async {})
2086 .on_qr_code(|_code, _timeout| async {})
2087 .on_event(|_event, _client| async {})
2088 .build()
2089 .await
2090 .expect("Failed to build bot");
2091
2092 assert_eq!(bot.event_handlers.len(), 3);
2093
2094 let interest = combined_interest(&bot.event_handlers);
2096 assert_eq!(interest, EventInterest::ALL);
2097 }
2098
2099 #[test]
2100 fn combined_interest_is_the_union_of_handler_interests() {
2101 let noop: EventHandlerCallback = Arc::new(|_event, _client| Box::pin(async {}));
2102 let handlers = vec![
2103 RegisteredHandler {
2104 callback: noop.clone(),
2105 interest: EventInterest::of(&[EventKind::Messages]),
2106 },
2107 RegisteredHandler {
2108 callback: noop,
2109 interest: EventInterest::of(&[EventKind::PairingQrCode]),
2110 },
2111 ];
2112
2113 let interest = combined_interest(&handlers);
2114 assert!(interest.wants(EventKind::Messages));
2115 assert!(interest.wants(EventKind::PairingQrCode));
2116 assert!(!interest.wants(EventKind::Receipt));
2117 }
2118
2119 #[tokio::test]
2120 async fn from_arc_does_not_deep_clone() {
2121 let backend = create_test_sqlite_backend().await;
2122 let bot = Bot::builder()
2123 .with_backend_arc(backend)
2124 .with_transport_factory(TokioWebSocketTransportFactory::new())
2125 .with_http_client(MockHttpClient)
2126 .with_runtime(TokioRuntime)
2127 .build()
2128 .await
2129 .expect("Failed to build bot");
2130
2131 let original = Arc::new(wa::Message {
2132 conversation: Some("ping".to_string()),
2133 ..Default::default()
2134 });
2135 let original_ptr = Arc::as_ptr(&original);
2136
2137 let ctx =
2138 MessageContext::from_arc(Arc::clone(&original), &MessageInfo::default(), bot.client());
2139
2140 assert!(std::ptr::eq(Arc::as_ptr(&ctx.message), original_ptr));
2141 }
2142
2143 async fn test_context_with_info(info: MessageInfo) -> MessageContext {
2144 let backend = create_test_sqlite_backend().await;
2145 let bot = Bot::builder()
2146 .with_backend_arc(backend)
2147 .with_transport_factory(TokioWebSocketTransportFactory::new())
2148 .with_http_client(MockHttpClient)
2149 .with_runtime(TokioRuntime)
2150 .build()
2151 .await
2152 .expect("Failed to build bot");
2153 MessageContext::from_arc(Arc::new(wa::Message::default()), &info, bot.client())
2154 }
2155
2156 fn react_info(chat: &str, sender: &str, id: &str, is_group: bool) -> MessageInfo {
2157 use crate::types::message::MessageSource;
2158 MessageInfo {
2159 id: id.to_string(),
2160 source: MessageSource {
2161 chat: chat.parse().expect("chat jid"),
2162 sender: sender.parse().expect("sender jid"),
2163 is_group,
2164 is_from_me: false,
2165 ..Default::default()
2166 },
2167 ..Default::default()
2168 }
2169 }
2170
2171 #[tokio::test]
2172 async fn react_target_key_carries_group_participant() {
2173 let info = react_info(
2174 "120363012345@g.us",
2175 "15551230000@s.whatsapp.net",
2176 "MSGID01",
2177 true,
2178 );
2179 let ctx = test_context_with_info(info).await;
2180 let key = ctx.message_key();
2181
2182 assert_eq!(key.remote_jid.as_deref(), Some("120363012345@g.us"));
2183 assert_eq!(key.id.as_deref(), Some("MSGID01"));
2184 assert_eq!(key.from_me, Some(false));
2185 assert_eq!(
2187 key.participant.as_deref(),
2188 Some("15551230000@s.whatsapp.net")
2189 );
2190 }
2191
2192 #[tokio::test]
2193 async fn react_target_key_omits_participant_in_dm() {
2194 let info = react_info(
2195 "15559990000@s.whatsapp.net",
2196 "15559990000@s.whatsapp.net",
2197 "MSGID02",
2198 false,
2199 );
2200 let ctx = test_context_with_info(info).await;
2201 let key = ctx.message_key();
2202
2203 assert_eq!(
2204 key.remote_jid.as_deref(),
2205 Some("15559990000@s.whatsapp.net")
2206 );
2207 assert!(key.participant.is_none());
2209 }
2210
2211 #[tokio::test]
2212 async fn react_target_key_carries_status_author() {
2213 let info = react_info(
2214 "status@broadcast",
2215 "15551112222@s.whatsapp.net",
2216 "MSGID03",
2217 false,
2218 );
2219 let ctx = test_context_with_info(info).await;
2220 let key = ctx.message_key();
2221
2222 assert_eq!(
2225 key.participant.as_deref(),
2226 Some("15551112222@s.whatsapp.net")
2227 );
2228 }
2229
2230 #[tokio::test]
2231 async fn run_metered_reports_to_instrument() {
2232 let meter = Arc::new(wacore::stats::CpuMeter::new());
2233 run_metered(
2234 async {
2235 tokio::task::yield_now().await;
2236 },
2237 Some(meter.clone()),
2238 )
2239 .await;
2240 assert!(meter.snapshot().polls >= 2);
2243 assert!(meter.snapshot().busy > std::time::Duration::ZERO);
2244
2245 run_metered(async {}, None).await;
2247 }
2248}