1use 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
19pub 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#[must_use]
32pub struct WeixinClientBuilder {
33 config: WeixinConfig,
34 handler: Option<Arc<dyn MessageHandler>>,
35 cancel: CancellationToken,
36}
37
38impl WeixinClient {
39 pub fn builder(config: WeixinConfig) -> WeixinClientBuilder {
41 WeixinClientBuilder {
42 config,
43 handler: None,
44 cancel: CancellationToken::new(),
45 }
46 }
47
48 pub async fn start(&self, initial_sync_buf: Option<String>) -> Result<()> {
52 if let Err(e) = self.api.notify_start().await {
53 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 pub fn shutdown(&self) {
77 self.cancel.cancel();
78 }
79
80 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 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 pub fn run(&self, to: &str, context_token: Option<&str>) -> OutboundRun {
107 self.sender.run(to, context_token)
108 }
109
110 pub fn qr_login(&self) -> QrLoginApi<'_> {
112 QrLoginApi::new(&self.api)
113 }
114
115 pub fn context_tokens(&self) -> &ContextTokenStore {
117 &self.context_tokens
118 }
119}
120
121impl WeixinClientBuilder {
122 pub fn on_message(mut self, handler: impl MessageHandler + 'static) -> Self {
124 self.handler = Some(Arc::new(handler));
125 self
126 }
127
128 pub fn with_cancel_token(mut self, cancel: CancellationToken) -> Self {
130 self.cancel = cancel;
131 self
132 }
133
134 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}