Skip to main content

weixin_agent/
client.rs

1//! SDK entry point: [`WeixinClient`] and its builder.
2
3use std::path::Path;
4use std::sync::Arc;
5
6use tokio_util::sync::CancellationToken;
7
8use crate::api::client::HttpApiClient;
9use crate::api::config_cache::ConfigCache;
10use crate::api::session_guard::SessionGuard;
11use crate::config::WeixinConfig;
12use crate::error::{Error, Result};
13use crate::messaging::inbound::{ContextTokenStore, SendResult};
14use crate::monitor::poll_loop::MessageHandler;
15use crate::qr_login::login::QrLoginApi;
16
17/// The main SDK client.
18pub struct WeixinClient {
19    config: Arc<WeixinConfig>,
20    handler: Arc<dyn MessageHandler>,
21    api: Arc<HttpApiClient>,
22    session_guard: Arc<SessionGuard>,
23    config_cache: Arc<ConfigCache>,
24    context_tokens: Arc<ContextTokenStore>,
25    cancel: CancellationToken,
26}
27
28/// Builder for [`WeixinClient`].
29#[must_use]
30pub struct WeixinClientBuilder {
31    config: WeixinConfig,
32    handler: Option<Arc<dyn MessageHandler>>,
33}
34
35impl WeixinClient {
36    /// Create a new builder.
37    pub fn builder(config: WeixinConfig) -> WeixinClientBuilder {
38        WeixinClientBuilder {
39            config,
40            handler: None,
41        }
42    }
43
44    /// Start the long-poll monitor loop. Blocks until shutdown.
45    ///
46    /// `initial_sync_buf` should be loaded from your persistence layer (or `None` for fresh start).
47    pub async fn start(&self, initial_sync_buf: Option<String>) -> Result<()> {
48        crate::monitor::poll_loop::run_monitor(
49            Arc::clone(&self.api),
50            self.config.cdn_base_url.clone(),
51            Arc::clone(&self.handler),
52            Arc::clone(&self.session_guard),
53            Arc::clone(&self.config_cache),
54            Arc::clone(&self.context_tokens),
55            initial_sync_buf,
56            self.config.long_poll_timeout,
57            self.cancel.clone(),
58        )
59        .await
60    }
61
62    /// Gracefully shut down the monitor loop.
63    pub fn shutdown(&self) {
64        self.cancel.cancel();
65    }
66
67    /// Send a text message to a user.
68    pub async fn send_text(
69        &self,
70        to: &str,
71        text: &str,
72        context_token: Option<&str>,
73    ) -> Result<SendResult> {
74        crate::messaging::send::send_text(&self.api, to, text, context_token).await
75    }
76
77    /// Send a media file to a user.
78    pub async fn send_media(
79        &self,
80        to: &str,
81        file_path: &Path,
82        context_token: Option<&str>,
83    ) -> Result<SendResult> {
84        crate::messaging::send_media::send_media_file(
85            &self.api,
86            &self.config.cdn_base_url,
87            to,
88            file_path,
89            "",
90            context_token,
91        )
92        .await
93    }
94
95    /// Get a QR login API handle.
96    pub fn qr_login(&self) -> QrLoginApi<'_> {
97        QrLoginApi::new(&self.api)
98    }
99
100    /// Access the context token store (for export/import).
101    pub fn context_tokens(&self) -> &ContextTokenStore {
102        &self.context_tokens
103    }
104}
105
106impl WeixinClientBuilder {
107    /// Set the message handler.
108    pub fn on_message(mut self, handler: impl MessageHandler + 'static) -> Self {
109        self.handler = Some(Arc::new(handler));
110        self
111    }
112
113    /// Build the client.
114    pub fn build(self) -> Result<WeixinClient> {
115        let handler = self
116            .handler
117            .ok_or_else(|| Error::Config("message handler is required".into()))?;
118        let api = Arc::new(HttpApiClient::new(&self.config));
119        let config_cache = Arc::new(ConfigCache::new(Arc::clone(&api)));
120        Ok(WeixinClient {
121            config: Arc::new(self.config),
122            handler,
123            api,
124            session_guard: Arc::new(SessionGuard::new()),
125            config_cache,
126            context_tokens: Arc::new(ContextTokenStore::new()),
127            cancel: CancellationToken::new(),
128        })
129    }
130}