1use crate::cache_config::CacheConfig;
2use crate::client::Client;
3use crate::pair_code::PairCodeOptions;
4use crate::store::commands::DeviceCommand;
5use crate::store::persistence_manager::PersistenceManager;
6use crate::store::traits::Backend;
7use crate::types::enc_handler::EncHandler;
8use crate::types::events::{Event, EventHandler};
9use crate::types::message::MessageInfo;
10use anyhow::Result;
11use log::{info, warn};
12use std::collections::HashMap;
13use std::future::Future;
14use std::marker::PhantomData;
15use std::pin::Pin;
16use std::sync::Arc;
17use thiserror::Error;
18use wacore::runtime::Runtime;
19use waproto::whatsapp as wa;
20
21pub struct Missing;
23pub struct Provided;
25
26#[derive(Debug, Error)]
27pub enum BotBuilderError {
28 #[error(transparent)]
29 Other(#[from] anyhow::Error),
30}
31
32pub struct MessageContext {
33 pub message: Box<wa::Message>,
34 pub info: MessageInfo,
35 pub client: Arc<Client>,
36}
37
38impl MessageContext {
39 pub async fn send_message(&self, message: wa::Message) -> Result<String, anyhow::Error> {
40 self.client
41 .send_message(self.info.source.chat.clone(), message)
42 .await
43 }
44
45 pub fn build_quote_context(&self) -> wa::ContextInfo {
54 wacore::proto_helpers::build_quote_context_with_info(
57 &self.info.id,
58 &self.info.source.sender,
59 &self.info.source.chat,
60 &self.message,
61 )
62 }
63
64 pub async fn edit_message(
65 &self,
66 original_message_id: impl Into<String>,
67 new_message: wa::Message,
68 ) -> Result<String, anyhow::Error> {
69 self.client
70 .edit_message(
71 self.info.source.chat.clone(),
72 original_message_id,
73 new_message,
74 )
75 .await
76 }
77
78 pub async fn revoke_message(
80 &self,
81 message_id: String,
82 revoke_type: crate::send::RevokeType,
83 ) -> Result<(), anyhow::Error> {
84 self.client
85 .revoke_message(self.info.source.chat.clone(), message_id, revoke_type)
86 .await
87 }
88}
89
90type EventHandlerCallback =
91 Arc<dyn Fn(Event, Arc<Client>) -> Pin<Box<dyn Future<Output = ()> + Send>> + Send + Sync>;
92
93struct BotEventHandler {
94 client: Arc<Client>,
95 event_handler: Option<EventHandlerCallback>,
96}
97
98impl EventHandler for BotEventHandler {
99 fn handle_event(&self, event: &Event) {
100 if let Some(handler) = &self.event_handler {
101 let handler_clone = handler.clone();
102 let event_clone = event.clone();
103 let client_clone = self.client.clone();
104
105 self.client
106 .runtime
107 .spawn(Box::pin(async move {
108 handler_clone(event_clone, client_clone).await;
109 }))
110 .detach();
111 }
112 }
113}
114
115pub struct BotHandle {
118 done_rx: futures::channel::oneshot::Receiver<()>,
119 _abort_handle: wacore::runtime::AbortHandle,
120}
121
122impl BotHandle {
123 pub fn abort(&self) {
125 self._abort_handle.abort();
126 }
127}
128
129impl std::future::Future for BotHandle {
130 type Output = Result<(), futures::channel::oneshot::Canceled>;
131
132 fn poll(
133 mut self: Pin<&mut Self>,
134 cx: &mut std::task::Context<'_>,
135 ) -> std::task::Poll<Self::Output> {
136 Pin::new(&mut self.done_rx).poll(cx)
137 }
138}
139
140pub struct Bot {
141 client: Arc<Client>,
142 sync_task_receiver: Option<async_channel::Receiver<crate::sync_task::MajorSyncTask>>,
143 event_handler: Option<EventHandlerCallback>,
144 pair_code_options: Option<PairCodeOptions>,
145}
146
147impl std::fmt::Debug for Bot {
148 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
149 f.debug_struct("Bot")
150 .field("client", &"<Client>")
151 .field("sync_task_receiver", &self.sync_task_receiver.is_some())
152 .field("event_handler", &self.event_handler.is_some())
153 .field("pair_code_options", &self.pair_code_options.is_some())
154 .finish()
155 }
156}
157
158impl Bot {
159 pub fn builder() -> BotBuilder<Missing, Missing, Missing, Missing> {
160 BotBuilder::new()
161 }
162
163 pub fn client(&self) -> Arc<Client> {
164 self.client.clone()
165 }
166
167 pub async fn run(&mut self) -> Result<BotHandle> {
168 if let Some(receiver) = self.sync_task_receiver.take() {
169 let worker_client = Arc::downgrade(&self.client);
170 self.client
171 .runtime
172 .spawn(Box::pin(async move {
173 while let Ok(task) = receiver.recv().await {
174 let Some(worker_client) = worker_client.upgrade() else {
175 break;
176 };
177
178 worker_client.process_sync_task(task).await;
179 }
180 info!("Sync worker shutting down.");
181 }))
182 .detach();
183 }
184
185 let handler = Arc::new(BotEventHandler {
186 client: self.client.clone(),
187 event_handler: self.event_handler.take(),
188 });
189 self.client.core.event_bus.add_handler(handler);
190
191 if let Some(options) = self.pair_code_options.take() {
193 let client_for_pair = self.client.clone();
194 self.client.runtime.spawn(Box::pin(async move {
195 if let Err(e) = client_for_pair
197 .wait_for_socket(std::time::Duration::from_secs(30))
198 .await
199 {
200 warn!(target: "Bot/PairCode", "Timeout waiting for socket: {}", e);
201 return;
202 }
203
204 if client_for_pair.is_logged_in() {
206 info!(target: "Bot/PairCode", "Already logged in, skipping pair code request");
207 return;
208 }
209
210 match client_for_pair.pair_with_code(options).await {
212 Ok(code) => {
213 info!(target: "Bot/PairCode", "Pair code generated: {}", code);
214 }
215 Err(e) => {
216 warn!(target: "Bot/PairCode", "Failed to request pair code: {}", e);
217 }
218 }
219 })).detach();
220 }
221
222 let client_for_run = self.client.clone();
223 let (done_tx, done_rx) = futures::channel::oneshot::channel::<()>();
224 let abort_handle = self.client.runtime.spawn(Box::pin(async move {
225 client_for_run.run().await;
226 let _ = done_tx.send(());
227 }));
228
229 Ok(BotHandle {
230 done_rx,
231 _abort_handle: abort_handle,
232 })
233 }
234}
235
236pub struct BotBuilder<B = Missing, T = Missing, H = Missing, R = Missing> {
243 backend: Option<Arc<dyn Backend>>,
245 transport_factory: Option<Arc<dyn crate::transport::TransportFactory>>,
246 http_client: Option<Arc<dyn crate::http::HttpClient>>,
247 runtime: Option<Arc<dyn Runtime>>,
248 event_handler: Option<EventHandlerCallback>,
250 custom_enc_handlers: HashMap<String, Arc<dyn EncHandler>>,
251 override_version: Option<(u32, u32, u32)>,
252 os_info: Option<(
253 Option<String>,
254 Option<wa::device_props::AppVersion>,
255 Option<wa::device_props::PlatformType>,
256 )>,
257 pair_code_options: Option<PairCodeOptions>,
258 skip_history_sync: bool,
259 initial_push_name: Option<String>,
260 cache_config: CacheConfig,
261 _marker: PhantomData<(B, T, H, R)>,
262}
263
264impl BotBuilder<Missing, Missing, Missing, Missing> {
265 fn new() -> Self {
266 Self {
267 backend: None,
268 transport_factory: None,
269 http_client: None,
270 runtime: None,
271 event_handler: None,
272 custom_enc_handlers: HashMap::new(),
273 override_version: None,
274 os_info: None,
275 pair_code_options: None,
276 skip_history_sync: false,
277 initial_push_name: None,
278 cache_config: CacheConfig::default(),
279 _marker: PhantomData,
280 }
281 }
282}
283
284impl<T, H, R> BotBuilder<Missing, T, H, R> {
287 pub fn with_backend(self, backend: Arc<dyn Backend>) -> BotBuilder<Provided, T, H, R> {
302 BotBuilder {
303 backend: Some(backend),
304 transport_factory: self.transport_factory,
305 http_client: self.http_client,
306 runtime: self.runtime,
307 event_handler: self.event_handler,
308 custom_enc_handlers: self.custom_enc_handlers,
309 override_version: self.override_version,
310 os_info: self.os_info,
311 pair_code_options: self.pair_code_options,
312 skip_history_sync: self.skip_history_sync,
313 initial_push_name: self.initial_push_name,
314 cache_config: self.cache_config,
315 _marker: PhantomData,
316 }
317 }
318}
319
320impl<B, H, R> BotBuilder<B, Missing, H, R> {
321 pub fn with_transport_factory<F>(self, factory: F) -> BotBuilder<B, Provided, H, R>
338 where
339 F: crate::transport::TransportFactory + 'static,
340 {
341 BotBuilder {
342 backend: self.backend,
343 transport_factory: Some(Arc::new(factory)),
344 http_client: self.http_client,
345 runtime: self.runtime,
346 event_handler: self.event_handler,
347 custom_enc_handlers: self.custom_enc_handlers,
348 override_version: self.override_version,
349 os_info: self.os_info,
350 pair_code_options: self.pair_code_options,
351 skip_history_sync: self.skip_history_sync,
352 initial_push_name: self.initial_push_name,
353 cache_config: self.cache_config,
354 _marker: PhantomData,
355 }
356 }
357}
358
359impl<B, T, R> BotBuilder<B, T, Missing, R> {
360 pub fn with_http_client<C>(self, client: C) -> BotBuilder<B, T, Provided, R>
376 where
377 C: crate::http::HttpClient + 'static,
378 {
379 BotBuilder {
380 backend: self.backend,
381 transport_factory: self.transport_factory,
382 http_client: Some(Arc::new(client)),
383 runtime: self.runtime,
384 event_handler: self.event_handler,
385 custom_enc_handlers: self.custom_enc_handlers,
386 override_version: self.override_version,
387 os_info: self.os_info,
388 pair_code_options: self.pair_code_options,
389 skip_history_sync: self.skip_history_sync,
390 initial_push_name: self.initial_push_name,
391 cache_config: self.cache_config,
392 _marker: PhantomData,
393 }
394 }
395}
396
397impl<B, T, H> BotBuilder<B, T, H, Missing> {
398 pub fn with_runtime<Rt: Runtime>(self, runtime: Rt) -> BotBuilder<B, T, H, Provided> {
402 BotBuilder {
403 backend: self.backend,
404 transport_factory: self.transport_factory,
405 http_client: self.http_client,
406 runtime: Some(Arc::new(runtime)),
407 event_handler: self.event_handler,
408 custom_enc_handlers: self.custom_enc_handlers,
409 override_version: self.override_version,
410 os_info: self.os_info,
411 pair_code_options: self.pair_code_options,
412 skip_history_sync: self.skip_history_sync,
413 initial_push_name: self.initial_push_name,
414 cache_config: self.cache_config,
415 _marker: PhantomData,
416 }
417 }
418}
419
420impl<B, T, H, R> BotBuilder<B, T, H, R> {
423 pub fn on_event<F, Fut>(mut self, handler: F) -> Self
424 where
425 F: Fn(Event, Arc<Client>) -> Fut + Send + Sync + 'static,
426 Fut: Future<Output = ()> + Send + 'static,
427 {
428 self.event_handler = Some(Arc::new(move |event, client| {
429 Box::pin(handler(event, client))
430 }));
431 self
432 }
433
434 pub fn with_enc_handler<Eh>(mut self, enc_type: impl Into<String>, handler: Eh) -> Self
443 where
444 Eh: EncHandler + 'static,
445 {
446 self.custom_enc_handlers
447 .insert(enc_type.into(), Arc::new(handler));
448 self
449 }
450
451 pub fn with_version(mut self, version: (u32, u32, u32)) -> Self {
468 self.override_version = Some(version);
469 self
470 }
471
472 pub fn with_device_props(
515 mut self,
516 os_name: Option<String>,
517 version: Option<wa::device_props::AppVersion>,
518 platform_type: Option<wa::device_props::PlatformType>,
519 ) -> Self {
520 self.os_info = Some((os_name, version, platform_type));
521 self
522 }
523
524 pub fn with_pair_code(mut self, options: PairCodeOptions) -> Self {
560 self.pair_code_options = Some(options);
561 self
562 }
563
564 pub fn skip_history_sync(mut self) -> Self {
586 self.skip_history_sync = true;
587 self
588 }
589
590 pub fn with_push_name(mut self, name: impl Into<String>) -> Self {
596 self.initial_push_name = Some(name.into());
597 self
598 }
599
600 pub fn with_cache_config(mut self, config: CacheConfig) -> Self {
623 self.cache_config = config;
624 self
625 }
626}
627
628impl BotBuilder<Provided, Provided, Provided, Provided> {
631 pub async fn build(self) -> std::result::Result<Bot, BotBuilderError> {
632 let (Some(runtime), Some(backend), Some(transport_factory), Some(http_client)) = (
634 self.runtime,
635 self.backend,
636 self.transport_factory,
637 self.http_client,
638 ) else {
639 unreachable!("typestate guarantees all required fields are Provided")
640 };
641
642 let persistence_manager = Arc::new(
645 PersistenceManager::new(backend)
646 .await
647 .map_err(|e| anyhow::anyhow!("Failed to create persistence manager: {}", e))?,
648 );
649
650 persistence_manager
651 .clone()
652 .run_background_saver(runtime.clone(), std::time::Duration::from_secs(30));
653
654 if let Some(name) = self.initial_push_name {
656 persistence_manager
657 .process_command(DeviceCommand::SetPushName(name))
658 .await;
659 }
660
661 if let Some((os_name, version, platform_type)) = self.os_info {
663 info!(
664 "Applying device props override: os={:?}, version={:?}, platform_type={:?}",
665 os_name, version, platform_type
666 );
667 persistence_manager
668 .process_command(DeviceCommand::SetDeviceProps(
669 os_name,
670 version,
671 platform_type,
672 ))
673 .await;
674 }
675
676 info!("Creating client...");
677 let (client, sync_task_receiver) = Client::new_with_cache_config(
678 runtime,
679 persistence_manager.clone(),
680 transport_factory,
681 http_client,
682 self.override_version,
683 self.cache_config,
684 )
685 .await;
686
687 for (enc_type, handler) in self.custom_enc_handlers {
689 client
690 .custom_enc_handlers
691 .write()
692 .await
693 .insert(enc_type, handler);
694 }
695
696 if self.skip_history_sync {
697 client.set_skip_history_sync(true);
698 }
699
700 Ok(Bot {
701 client,
702 sync_task_receiver: Some(sync_task_receiver),
703 event_handler: self.event_handler,
704 pair_code_options: self.pair_code_options,
705 })
706 }
707}
708
709#[cfg(test)]
710mod tests {
711 use super::*;
712 use crate::TokioRuntime;
713 use crate::http::{HttpClient, HttpRequest, HttpResponse};
714 use crate::store::SqliteStore;
715 use whatsapp_rust_tokio_transport::TokioWebSocketTransportFactory;
716
717 #[derive(Debug, Clone)]
719 struct MockHttpClient;
720
721 #[async_trait::async_trait]
722 impl HttpClient for MockHttpClient {
723 async fn execute(&self, _request: HttpRequest) -> Result<HttpResponse> {
724 Ok(HttpResponse {
726 status_code: 200,
727 body: br#"self.__swData=JSON.parse(/*BTDS*/"{\"dynamic_data\":{\"SiteData\":{\"server_revision\":1026131876,\"client_revision\":1026131876}}}");"#.to_vec(),
728 })
729 }
730 }
731
732 async fn create_test_sqlite_backend() -> Arc<dyn Backend> {
733 let temp_db = format!(
734 "file:memdb_bot_{}?mode=memory&cache=shared",
735 uuid::Uuid::new_v4()
736 );
737 Arc::new(
738 SqliteStore::new(&temp_db)
739 .await
740 .expect("Failed to create test SqliteStore"),
741 ) as Arc<dyn Backend>
742 }
743
744 async fn create_test_sqlite_backend_for_device(device_id: i32) -> Arc<dyn Backend> {
745 let temp_db = format!(
746 "file:memdb_bot_{}?mode=memory&cache=shared",
747 uuid::Uuid::new_v4()
748 );
749 Arc::new(
750 SqliteStore::new_for_device(&temp_db, device_id)
751 .await
752 .expect("Failed to create test SqliteStore"),
753 ) as Arc<dyn Backend>
754 }
755
756 #[tokio::test]
757 async fn test_bot_builder_single_device() {
758 let backend = create_test_sqlite_backend().await;
759 let transport = TokioWebSocketTransportFactory::new();
760 let http_client = MockHttpClient;
761
762 let bot = Bot::builder()
763 .with_backend(backend)
764 .with_transport_factory(transport)
765 .with_http_client(http_client)
766 .with_runtime(TokioRuntime)
767 .build()
768 .await
769 .expect("Failed to build bot");
770
771 let _client = bot.client();
773 }
774
775 #[tokio::test]
776 async fn test_bot_builder_multi_device() {
777 let backend = create_test_sqlite_backend_for_device(42).await;
779 let transport = TokioWebSocketTransportFactory::new();
780
781 let bot = Bot::builder()
782 .with_backend(backend)
783 .with_transport_factory(transport)
784 .with_http_client(MockHttpClient)
785 .with_runtime(TokioRuntime)
786 .build()
787 .await
788 .expect("Failed to build bot");
789
790 let _client = bot.client();
792 }
793
794 #[tokio::test]
795 async fn test_bot_builder_with_custom_backend() {
796 let backend = create_test_sqlite_backend().await;
798 let transport = TokioWebSocketTransportFactory::new();
799 let http_client = MockHttpClient;
800 let bot = Bot::builder()
801 .with_backend(backend)
802 .with_transport_factory(transport)
803 .with_http_client(http_client)
804 .with_runtime(TokioRuntime)
805 .build()
806 .await
807 .expect("Failed to build bot with custom backend");
808
809 let _client = bot.client();
811 }
812
813 #[tokio::test]
814 async fn test_bot_builder_with_custom_backend_specific_device() {
815 let backend = create_test_sqlite_backend_for_device(100).await;
817 let transport = TokioWebSocketTransportFactory::new();
818 let http_client = MockHttpClient;
819
820 let bot = Bot::builder()
822 .with_backend(backend)
823 .with_http_client(http_client)
824 .with_transport_factory(transport)
825 .with_runtime(TokioRuntime)
826 .build()
827 .await
828 .expect("Failed to build bot with custom backend for specific device");
829
830 let _client = bot.client();
832 }
833
834 #[tokio::test]
840 async fn test_bot_builder_with_version_override() {
841 let backend = create_test_sqlite_backend().await;
842 let transport = TokioWebSocketTransportFactory::new();
843 let http_client = MockHttpClient;
844
845 let bot = Bot::builder()
846 .with_backend(backend)
847 .with_transport_factory(transport)
848 .with_http_client(http_client)
849 .with_version((2, 3000, 123456789))
850 .with_runtime(TokioRuntime)
851 .build()
852 .await
853 .expect("Failed to build bot with version override");
854
855 let client = bot.client();
857
858 assert_eq!(client.override_version, Some((2, 3000, 123456789)));
860 }
861
862 #[tokio::test]
863 async fn test_bot_builder_with_device_props_override() {
864 let backend = create_test_sqlite_backend().await;
865 let transport = TokioWebSocketTransportFactory::new();
866 let http_client = MockHttpClient;
867
868 let custom_os = "CustomOS".to_string();
869 let custom_version = wa::device_props::AppVersion {
870 primary: Some(99),
871 secondary: Some(88),
872 tertiary: Some(77),
873 ..Default::default()
874 };
875
876 let bot = Bot::builder()
877 .with_backend(backend)
878 .with_transport_factory(transport)
879 .with_http_client(http_client)
880 .with_device_props(Some(custom_os.clone()), Some(custom_version), None)
881 .with_runtime(TokioRuntime)
882 .build()
883 .await
884 .expect("Failed to build bot with device props override");
885
886 let client = bot.client();
887 let persistence_manager = client.persistence_manager();
888 let device = persistence_manager.get_device_snapshot().await;
889
890 assert_eq!(device.device_props.os, Some(custom_os));
892 assert_eq!(device.device_props.version, Some(custom_version));
893 }
894
895 #[tokio::test]
896 async fn test_bot_builder_with_os_only_override() {
897 let backend = create_test_sqlite_backend().await;
898 let transport = TokioWebSocketTransportFactory::new();
899 let http_client = MockHttpClient;
900
901 let custom_os = "CustomOS".to_string();
902
903 let bot = Bot::builder()
904 .with_backend(backend)
905 .with_transport_factory(transport)
906 .with_http_client(http_client)
907 .with_device_props(Some(custom_os.clone()), None, None)
908 .with_runtime(TokioRuntime)
909 .build()
910 .await
911 .expect("Failed to build bot with OS only override");
912
913 let client = bot.client();
914 let persistence_manager = client.persistence_manager();
915 let device = persistence_manager.get_device_snapshot().await;
916
917 assert_eq!(device.device_props.os, Some(custom_os));
919 assert_eq!(
921 device.device_props.version,
922 Some(wacore::store::Device::default_device_props_version())
923 );
924 }
925
926 #[tokio::test]
927 async fn test_bot_builder_with_version_only_override() {
928 let backend = create_test_sqlite_backend().await;
929 let transport = TokioWebSocketTransportFactory::new();
930 let http_client = MockHttpClient;
931
932 let custom_version = wa::device_props::AppVersion {
933 primary: Some(99),
934 secondary: Some(88),
935 tertiary: Some(77),
936 ..Default::default()
937 };
938
939 let bot = Bot::builder()
940 .with_backend(backend)
941 .with_http_client(http_client)
942 .with_transport_factory(transport)
943 .with_device_props(None, Some(custom_version), None)
944 .with_runtime(TokioRuntime)
945 .build()
946 .await
947 .expect("Failed to build bot with version only override");
948
949 let client = bot.client();
950 let persistence_manager = client.persistence_manager();
951 let device = persistence_manager.get_device_snapshot().await;
952
953 assert_eq!(device.device_props.version, Some(custom_version));
955 assert_eq!(
957 device.device_props.os,
958 Some(wacore::store::Device::default_os().to_string())
959 );
960 }
961
962 #[tokio::test]
963 async fn test_bot_builder_with_platform_type_override() {
964 let backend = create_test_sqlite_backend().await;
965 let transport = TokioWebSocketTransportFactory::new();
966 let http_client = MockHttpClient;
967
968 let bot = Bot::builder()
969 .with_backend(backend)
970 .with_transport_factory(transport)
971 .with_http_client(http_client)
972 .with_device_props(None, None, Some(wa::device_props::PlatformType::Chrome))
973 .with_runtime(TokioRuntime)
974 .build()
975 .await
976 .expect("Failed to build bot with platform type override");
977
978 let client = bot.client();
979 let persistence_manager = client.persistence_manager();
980 let device = persistence_manager.get_device_snapshot().await;
981
982 assert_eq!(
984 device.device_props.platform_type,
985 Some(wa::device_props::PlatformType::Chrome as i32)
986 );
987 assert_eq!(
989 device.device_props.os,
990 Some(wacore::store::Device::default_os().to_string())
991 );
992 assert_eq!(
993 device.device_props.version,
994 Some(wacore::store::Device::default_device_props_version())
995 );
996 }
997
998 #[tokio::test]
999 async fn test_bot_builder_with_full_device_props_override() {
1000 let backend = create_test_sqlite_backend().await;
1001 let transport = TokioWebSocketTransportFactory::new();
1002 let http_client = MockHttpClient;
1003
1004 let custom_os = "macOS".to_string();
1005 let custom_version = wa::device_props::AppVersion {
1006 primary: Some(2),
1007 secondary: Some(0),
1008 tertiary: Some(0),
1009 ..Default::default()
1010 };
1011 let custom_platform = wa::device_props::PlatformType::Safari;
1012
1013 let bot = Bot::builder()
1014 .with_backend(backend)
1015 .with_transport_factory(transport)
1016 .with_http_client(http_client)
1017 .with_device_props(
1018 Some(custom_os.clone()),
1019 Some(custom_version),
1020 Some(custom_platform),
1021 )
1022 .with_runtime(TokioRuntime)
1023 .build()
1024 .await
1025 .expect("Failed to build bot with full device props override");
1026
1027 let client = bot.client();
1028 let persistence_manager = client.persistence_manager();
1029 let device = persistence_manager.get_device_snapshot().await;
1030
1031 assert_eq!(device.device_props.os, Some(custom_os));
1033 assert_eq!(device.device_props.version, Some(custom_version));
1034 assert_eq!(
1035 device.device_props.platform_type,
1036 Some(custom_platform as i32)
1037 );
1038 }
1039
1040 #[tokio::test]
1041 async fn test_bot_builder_skip_history_sync() {
1042 let backend = create_test_sqlite_backend().await;
1043 let transport = TokioWebSocketTransportFactory::new();
1044 let http_client = MockHttpClient;
1045
1046 let bot = Bot::builder()
1047 .with_backend(backend)
1048 .with_transport_factory(transport)
1049 .with_http_client(http_client)
1050 .skip_history_sync()
1051 .with_runtime(TokioRuntime)
1052 .build()
1053 .await
1054 .expect("Failed to build bot with skip_history_sync");
1055
1056 assert!(bot.client().skip_history_sync_enabled());
1057 }
1058
1059 #[tokio::test]
1060 async fn test_bot_builder_default_history_sync_enabled() {
1061 let backend = create_test_sqlite_backend().await;
1062 let transport = TokioWebSocketTransportFactory::new();
1063 let http_client = MockHttpClient;
1064
1065 let bot = Bot::builder()
1066 .with_backend(backend)
1067 .with_transport_factory(transport)
1068 .with_http_client(http_client)
1069 .with_runtime(TokioRuntime)
1070 .build()
1071 .await
1072 .expect("Failed to build bot");
1073
1074 assert!(!bot.client().skip_history_sync_enabled());
1075 }
1076}