Skip to main content

whatsapp_rust/
bot.rs

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
21/// Typestate marker: a required builder field has not been provided yet.
22pub struct Missing;
23/// Typestate marker: a required builder field has been provided.
24pub 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    /// Build a quote context for this message.
46    ///
47    /// Handles:
48    /// - Correct stanza_id/participant (newsletters + group status)
49    /// - Stripping nested mentions to avoid accidental tags
50    /// - Preserving bot quote chains (matches WhatsApp Web)
51    ///
52    /// Use this when you need manual control but want correct quoting behavior.
53    pub fn build_quote_context(&self) -> wa::ContextInfo {
54        // Use the standalone function from wacore with full message info
55        // This handles newsletter/group status participant resolution
56        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    /// Delete a message for everyone in the chat.
79    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
115/// Handle returned by [`Bot::run`] that can be awaited to wait for the
116/// client's run loop to finish.
117pub struct BotHandle {
118    done_rx: futures::channel::oneshot::Receiver<()>,
119    _abort_handle: wacore::runtime::AbortHandle,
120}
121
122impl BotHandle {
123    /// Abort the bot's run task.
124    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 pair code options are set, spawn a task to request pair code after socket is ready
192        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                // Wait for socket to be ready (before login) with 30 second timeout
196                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                // Check if already logged in (paired via QR or existing session)
205                if client_for_pair.is_logged_in() {
206                    info!(target: "Bot/PairCode", "Already logged in, skipping pair code request");
207                    return;
208                }
209
210                // Request pair code
211                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
236/// Builder for [`Bot`] using the typestate pattern.
237///
238/// The four type parameters (`B`, `T`, `H`, `R`) track whether the required
239/// fields (backend, transport_factory, http_client, runtime) have been
240/// provided. The `build()` method is only available when all four are
241/// [`Provided`], turning missing-field errors into compile-time errors.
242pub struct BotBuilder<B = Missing, T = Missing, H = Missing, R = Missing> {
243    // Required fields (guaranteed present when B/T/H/R = Provided)
244    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    // Optional fields
249    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
284// ── Required-field setters (each transitions one type parameter) ──────────
285
286impl<T, H, R> BotBuilder<Missing, T, H, R> {
287    /// Use a backend implementation for storage.
288    /// This is the only way to configure storage - there are no defaults.
289    ///
290    /// # Arguments
291    /// * `backend` - The backend implementation that provides all storage operations
292    ///
293    /// # Example
294    /// ```rust,ignore
295    /// let backend = Arc::new(SqliteStore::new("whatsapp.db").await?);
296    /// let bot = Bot::builder()
297    ///     .with_backend(backend)
298    ///     .build()
299    ///     .await?;
300    /// ```
301    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    /// Set the transport factory for creating network connections.
322    /// This is required to build a bot.
323    ///
324    /// # Arguments
325    /// * `factory` - The transport factory implementation
326    ///
327    /// # Example
328    /// ```rust,ignore
329    /// use whatsapp_rust_tokio_transport::TokioWebSocketTransportFactory;
330    ///
331    /// let bot = Bot::builder()
332    ///     .with_backend(backend)
333    ///     .with_transport_factory(TokioWebSocketTransportFactory::new())
334    ///     .build()
335    ///     .await?;
336    /// ```
337    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    /// Configure the HTTP client used for media operations and version fetching.
361    ///
362    /// # Arguments
363    /// * `client` - The HTTP client implementation
364    ///
365    /// # Example
366    /// ```rust,ignore
367    /// use whatsapp_rust_ureq_http_client::UreqHttpClient;
368    ///
369    /// let bot = Bot::builder()
370    ///     .with_backend(backend)
371    ///     .with_http_client(UreqHttpClient::new())
372    ///     .build()
373    ///     .await?;
374    /// ```
375    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    /// Set the async runtime implementation to use.
399    ///
400    /// This is required to build a bot.
401    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
420// ── Optional-field setters (available in any state) ──────────────────────
421
422impl<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    /// Register a custom handler for a specific encrypted message type
435    ///
436    /// # Arguments
437    /// * `enc_type` - The encrypted message type (e.g., "frskmsg")
438    /// * `handler` - The handler implementation for this type
439    ///
440    /// # Returns
441    /// The updated BotBuilder
442    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    /// Override the WhatsApp version used by the client.
452    ///
453    /// By default, the client will automatically fetch the latest version from WhatsApp's servers.
454    /// Use this method to force a specific version instead.
455    ///
456    /// # Arguments
457    /// * `version` - A tuple of (primary, secondary, tertiary) version numbers
458    ///
459    /// # Example
460    /// ```rust,ignore
461    /// let bot = Bot::builder()
462    ///     .with_backend(backend)
463    ///     .with_version((2, 3000, 1027868167))
464    ///     .build()
465    ///     .await?;
466    /// ```
467    pub fn with_version(mut self, version: (u32, u32, u32)) -> Self {
468        self.override_version = Some(version);
469        self
470    }
471
472    /// Override the device properties sent to WhatsApp servers.
473    /// This allows customizing how your device appears on the linked devices list.
474    ///
475    /// # Arguments
476    /// * `os_name` - Optional OS name (e.g., "macOS", "Windows", "Linux")
477    /// * `version` - Optional app version as AppVersion struct
478    /// * `platform_type` - Optional platform type that determines the device name shown
479    ///   on the phone's linked devices list (e.g., Chrome, Firefox, Safari, Desktop)
480    ///
481    /// **Important**: The `platform_type` determines what device name is shown on the phone.
482    /// Common values: `Chrome`, `Firefox`, `Safari`, `Edge`, `Desktop`, `Ipad`, etc.
483    /// If not set, defaults to `Unknown` which shows as "Unknown device".
484    ///
485    /// You can pass `None` for any parameter to keep the default value.
486    ///
487    /// # Example
488    /// ```rust,ignore
489    /// use waproto::whatsapp::device_props::{self, PlatformType};
490    ///
491    /// // Show as "Chrome" on linked devices
492    /// let bot = Bot::builder()
493    ///     .with_backend(backend)
494    ///     .with_device_props(
495    ///         Some("macOS".to_string()),
496    ///         Some(device_props::AppVersion {
497    ///             primary: Some(2),
498    ///             secondary: Some(0),
499    ///             tertiary: Some(0),
500    ///             ..Default::default()
501    ///         }),
502    ///         Some(PlatformType::Chrome),
503    ///     )
504    ///     .build()
505    ///     .await?;
506    ///
507    /// // Show as "Desktop" on linked devices
508    /// let bot = Bot::builder()
509    ///     .with_backend(backend)
510    ///     .with_device_props(None, None, Some(PlatformType::Desktop))
511    ///     .build()
512    ///     .await?;
513    /// ```
514    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    /// Configure pair code authentication to run automatically after connecting.
525    ///
526    /// When set, the pair code request will be sent automatically after establishing
527    /// a connection, and the pairing code will be dispatched via `Event::PairingCode`.
528    /// This runs concurrently with QR code pairing - whichever completes first wins.
529    ///
530    /// # Arguments
531    /// * `options` - Configuration for pair code authentication
532    ///
533    /// # Example
534    /// ```rust,ignore
535    /// use whatsapp_rust::pair_code::{PairCodeOptions, PlatformId};
536    ///
537    /// let bot = Bot::builder()
538    ///     .with_backend(backend)
539    ///     .with_transport_factory(transport)
540    ///     .with_http_client(http_client)
541    ///     .with_pair_code(PairCodeOptions {
542    ///         phone_number: "15551234567".to_string(),
543    ///         show_push_notification: true,
544    ///         custom_code: Some("ABCD1234".to_string()),
545    ///         platform_id: PlatformId::Chrome,
546    ///         platform_display: "Chrome (Linux)".to_string(),
547    ///     })
548    ///     .on_event(|event, client| async move {
549    ///         match event {
550    ///             Event::PairingCode { code, timeout } => {
551    ///                 println!("Enter this code on your phone: {}", code);
552    ///             }
553    ///             _ => {}
554    ///         }
555    ///     })
556    ///     .build()
557    ///     .await?;
558    /// ```
559    pub fn with_pair_code(mut self, options: PairCodeOptions) -> Self {
560        self.pair_code_options = Some(options);
561        self
562    }
563
564    /// Skip processing of history sync notifications from the phone.
565    ///
566    /// When enabled, the client will acknowledge all incoming history sync
567    /// notifications (so the phone considers them delivered) but will not
568    /// download or process any historical data (INITIAL_BOOTSTRAP, RECENT,
569    /// FULL, PUSH_NAME, etc.). A debug log entry is emitted for each skipped
570    /// notification. This is useful for bot use cases where message history
571    /// is not needed.
572    ///
573    /// Default: `false` (history sync is processed normally).
574    ///
575    /// # Example
576    /// ```rust,ignore
577    /// let bot = Bot::builder()
578    ///     .with_backend(backend)
579    ///     .with_transport_factory(transport)
580    ///     .with_http_client(http_client)
581    ///     .skip_history_sync()
582    ///     .build()
583    ///     .await?;
584    /// ```
585    pub fn skip_history_sync(mut self) -> Self {
586        self.skip_history_sync = true;
587        self
588    }
589
590    /// Set an initial push name on the device before connecting.
591    ///
592    /// This is included in the `ClientPayload` during registration, allowing the
593    /// mock server to deterministically assign phone numbers based on push name
594    /// (same push name = same phone, enabling multi-device testing).
595    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    /// Configure cache TTL and capacity settings.
601    ///
602    /// By default, all caches match WhatsApp Web behavior. Use this method
603    /// to customize cache durations for your use case.
604    ///
605    /// # Example
606    /// ```rust,ignore
607    /// use whatsapp_rust::{CacheConfig, CacheEntryConfig};
608    ///
609    /// // Disable TTL for group and device caches (good for bots with few groups)
610    /// let bot = Bot::builder()
611    ///     .with_backend(backend)
612    ///     .with_transport_factory(transport)
613    ///     .with_http_client(http_client)
614    ///     .with_cache_config(CacheConfig {
615    ///         group_cache: CacheEntryConfig::new(None, 1_000),
616    ///         device_cache: CacheEntryConfig::new(None, 5_000),
617    ///         ..Default::default()
618    ///     })
619    ///     .build()
620    ///     .await?;
621    /// ```
622    pub fn with_cache_config(mut self, config: CacheConfig) -> Self {
623        self.cache_config = config;
624        self
625    }
626}
627
628// ── build() — only available when all 4 required fields are Provided ─────
629
630impl BotBuilder<Provided, Provided, Provided, Provided> {
631    pub async fn build(self) -> std::result::Result<Bot, BotBuilderError> {
632        // Destructure to extract required fields — typestate guarantees all are Some.
633        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        // Note: For multi-account mode, create the backend with SqliteStore::new_for_device()
643        // before passing it to with_backend()
644        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        // Apply initial push name if specified (for deterministic mock server phone assignment)
655        if let Some(name) = self.initial_push_name {
656            persistence_manager
657                .process_command(DeviceCommand::SetPushName(name))
658                .await;
659        }
660
661        // Apply device props override if specified
662        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        // Register custom enc handlers
688        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    // Mock HTTP client for testing
718    #[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            // Return a mock response for version fetching
725            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        // Verify bot was created successfully
772        let _client = bot.client();
773    }
774
775    #[tokio::test]
776    async fn test_bot_builder_multi_device() {
777        // Create a backend configured for device ID 42
778        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        // Verify bot was created successfully
791        let _client = bot.client();
792    }
793
794    #[tokio::test]
795    async fn test_bot_builder_with_custom_backend() {
796        // Create an in-memory backend for testing
797        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        // Verify the bot was created successfully
810        let _client = bot.client();
811    }
812
813    #[tokio::test]
814    async fn test_bot_builder_with_custom_backend_specific_device() {
815        // Create a backend configured for device ID 100
816        let backend = create_test_sqlite_backend_for_device(100).await;
817        let transport = TokioWebSocketTransportFactory::new();
818        let http_client = MockHttpClient;
819
820        // Build a bot with the custom backend
821        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        // Verify the bot was created successfully
831        let _client = bot.client();
832    }
833
834    // NOTE: test_bot_builder_missing_backend, test_bot_builder_missing_transport,
835    // and test_bot_builder_missing_http_client have been removed because the
836    // typestate pattern now makes those cases compile-time errors instead of
837    // runtime errors.
838
839    #[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        // Verify the bot was created successfully
856        let client = bot.client();
857
858        // Check that the override version is stored in the client
859        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        // Verify the device props were overridden
891        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        // Verify only OS was overridden, version should be default
918        assert_eq!(device.device_props.os, Some(custom_os));
919        // Version should be the default since we didn't override it
920        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        // Verify only version was overridden, OS should be default ("rust")
954        assert_eq!(device.device_props.version, Some(custom_version));
955        // OS should be the default since we didn't override it
956        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        // Verify platform type was set to Chrome
983        assert_eq!(
984            device.device_props.platform_type,
985            Some(wa::device_props::PlatformType::Chrome as i32)
986        );
987        // OS and version should remain default
988        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        // Verify all device props were overridden
1032        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}