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::messaging::outbound_run::OutboundRun;
15use crate::messaging::sender::MessageSender;
16use crate::monitor::poll_loop::MessageHandler;
17use crate::qr_login::login::QrLoginApi;
18
19/// The main SDK client.
20pub struct WeixinClient {
21    config: Arc<WeixinConfig>,
22    handler: Arc<dyn MessageHandler>,
23    api: Arc<HttpApiClient>,
24    sender: Arc<MessageSender>,
25    session_guard: Arc<SessionGuard>,
26    context_tokens: Arc<ContextTokenStore>,
27    cancel: CancellationToken,
28}
29
30/// Builder for [`WeixinClient`].
31#[must_use]
32pub struct WeixinClientBuilder {
33    config: WeixinConfig,
34    handler: Option<Arc<dyn MessageHandler>>,
35    cancel: CancellationToken,
36}
37
38impl WeixinClient {
39    /// Create a new builder.
40    pub fn builder(config: WeixinConfig) -> WeixinClientBuilder {
41        WeixinClientBuilder {
42            config,
43            handler: None,
44            cancel: CancellationToken::new(),
45        }
46    }
47
48    /// Start the long-poll monitor loop. Blocks until shutdown.
49    ///
50    /// `initial_sync_buf` should be loaded from your persistence layer (or `None` for fresh start).
51    pub async fn start(&self, initial_sync_buf: Option<String>) -> Result<()> {
52        if let Err(e) = self.api.notify_start().await {
53            // Best-effort call; `post_raw` already logged the classified transport
54            // failure. The error text is not repeated here because it can carry an
55            // un-redacted URL (standards §1.3).
56            tracing::warn!(
57                kind = crate::util::net_error::classify(&e).as_str(),
58                "notify_start failed"
59            );
60        }
61
62        crate::monitor::poll_loop::run_monitor(
63            Arc::clone(&self.api),
64            Arc::clone(&self.sender),
65            Arc::clone(&self.handler),
66            Arc::clone(&self.session_guard),
67            Arc::clone(&self.context_tokens),
68            initial_sync_buf,
69            self.config.long_poll_timeout,
70            self.cancel.clone(),
71        )
72        .await
73    }
74
75    /// Gracefully shut down the monitor loop.
76    pub fn shutdown(&self) {
77        self.cancel.cancel();
78    }
79
80    /// Send a text message to a user.
81    pub async fn send_text(
82        &self,
83        to: &str,
84        text: &str,
85        context_token: Option<&str>,
86    ) -> Result<SendResult> {
87        self.sender.send_text(to, text, context_token, None).await
88    }
89
90    /// Send a media file to a user.
91    pub async fn send_media(
92        &self,
93        to: &str,
94        file_path: &Path,
95        context_token: Option<&str>,
96    ) -> Result<SendResult> {
97        self.sender
98            .send_media(to, file_path, context_token, None)
99            .await
100    }
101
102    /// Start an outbound run addressed to `to`.
103    ///
104    /// Every message sent through the returned handle carries the same `run_id`,
105    /// which lets the peer group them as one logical run.
106    pub fn run(&self, to: &str, context_token: Option<&str>) -> OutboundRun {
107        self.sender.run(to, context_token)
108    }
109
110    /// Get a QR login API handle.
111    pub fn qr_login(&self) -> QrLoginApi<'_> {
112        QrLoginApi::new(&self.api)
113    }
114
115    /// Access the context token store (for export/import).
116    pub fn context_tokens(&self) -> &ContextTokenStore {
117        &self.context_tokens
118    }
119}
120
121impl WeixinClientBuilder {
122    /// Set the message handler.
123    pub fn on_message(mut self, handler: impl MessageHandler + 'static) -> Self {
124        self.handler = Some(Arc::new(handler));
125        self
126    }
127
128    /// Optionally set a cancellation token for the monitor loop (for advanced users).
129    pub fn with_cancel_token(mut self, cancel: CancellationToken) -> Self {
130        self.cancel = cancel;
131        self
132    }
133
134    /// Build the client.
135    pub fn build(self) -> Result<WeixinClient> {
136        let handler = self
137            .handler
138            .ok_or_else(|| Error::Config("message handler is required".into()))?;
139        let api = Arc::new(HttpApiClient::new(&self.config));
140        let config_cache = Arc::new(ConfigCache::new(Arc::clone(&api)));
141        let sender = Arc::new(MessageSender {
142            api: Arc::clone(&api),
143            cdn_base_url: self.config.cdn_base_url.clone(),
144            config_cache,
145            markdown_filter_enabled: self.config.markdown_filter_enabled,
146        });
147        Ok(WeixinClient {
148            config: Arc::new(self.config),
149            handler,
150            api,
151            sender,
152            session_guard: Arc::new(SessionGuard::new()),
153            context_tokens: Arc::new(ContextTokenStore::new()),
154            cancel: self.cancel,
155        })
156    }
157}