sms_client/
lib.rs

1//! A client library for SMS-API, via HTTP and an optional websocket connection.
2//! <https://github.com/morgverd/sms-server>
3
4#![deny(missing_docs)]
5#![deny(unsafe_code)]
6#![warn(clippy::all, clippy::pedantic)]
7
8use crate::error::*;
9
10pub mod config;
11pub mod error;
12pub mod types;
13
14#[cfg(feature = "http")]
15pub mod http;
16
17#[cfg(feature = "websocket")]
18pub mod ws;
19
20/// SMS Client.
21#[derive(Clone, Debug)]
22pub struct Client {
23
24    #[cfg(feature = "http")]
25    http_client: Option<std::sync::Arc<http::HttpClient>>,
26
27    #[cfg(feature = "websocket")]
28    ws_client: Option<std::sync::Arc<tokio::sync::Mutex<ws::WebSocketClient>>>
29}
30impl Client {
31
32    /// Create an SMS client with a connection config.
33    pub fn new(config: config::ClientConfig) -> ClientResult<Self> {
34        let tls = config.tls;
35
36        #[cfg(feature = "http")]
37        let http_client = if let Some(http_config) = config.http {
38            Some(std::sync::Arc::new(
39                http::HttpClient::new(http_config, &tls)?
40            ))
41        } else {
42            None
43        };
44
45        #[cfg(feature = "websocket")]
46        let ws_client = config.websocket.map(|ws_config| {
47            std::sync::Arc::new(tokio::sync::Mutex::new(
48                ws::WebSocketClient::new(ws_config, tls)
49            ))
50        });
51
52        Ok(Self {
53            #[cfg(feature = "http")]
54            http_client,
55
56            #[cfg(feature = "websocket")]
57            ws_client
58        })
59    }
60
61    /// Borrow the optional inner HTTP client.
62    #[cfg(feature = "http")]
63    pub fn http(&self) -> ClientResult<&http::HttpClient> {
64        self.http_client
65            .as_ref()
66            .map(|arc| arc.as_ref())
67            .ok_or(ClientError::ConfigError("HttpClient"))
68    }
69
70    /// Get a cloned Arc to the optional HTTP client for use in async contexts.
71    #[cfg(feature = "http")]
72    pub fn http_arc(&self) -> ClientResult<std::sync::Arc<http::HttpClient>> {
73        self.http_client
74            .clone()
75            .ok_or(ClientError::ConfigError("HttpClient"))
76    }
77
78    /// Set the callback for incoming WebSocket messages. The callback will include the WebSocket
79    /// message and an Arc to the current Client allowing for easy use within the callback!
80    /// This must be called before starting the WebSocket connection.
81    ///
82    /// # Example
83    /// ```
84    /// use sms_client::http::types::HttpOutgoingSmsMessage;
85    /// use sms_client::ws::types::WebsocketMessage;
86    /// use sms_client::Client;
87    /// use log::info;
88    ///
89    /// #[tokio::main]
90    /// async fn main() {
91    ///     let client: Client = unimplemented!("See other examples");
92    ///
93    ///     client.on_message(move |message, client| {
94    ///         match message {
95    ///             WebsocketMessage::IncomingMessage(sms) => {
96    ///                 // Can access client.http() here!
97    ///             },
98    ///             _ => { }
99    ///         }
100    ///     }).await?
101    /// }
102    /// ```
103    #[cfg(feature = "websocket")]
104    pub async fn on_message<F>(&self, callback: F) -> ClientResult<()>
105    where
106        F: Fn(ws::types::WebsocketMessage, std::sync::Arc<Self>) + Send + Sync + 'static,
107    {
108        let ws_client = self.ws_client
109            .as_ref()
110            .ok_or(ClientError::ConfigError("WebSocketClient"))?;
111
112        let mut ws_guard = ws_client.lock().await;
113        let client_arc = std::sync::Arc::new(self.clone());
114
115        ws_guard.on_message(move |msg| {
116            callback(msg, std::sync::Arc::clone(&client_arc))
117        });
118
119        Ok(())
120    }
121
122    /// Set the callback for incoming WebSocket messages (simple version without client copy).
123    /// This must be called before starting the WebSocket connection.
124    ///
125    /// # Example
126    /// ```
127    /// use sms_client::Client;
128    /// use sms_client::ws::types::WebsocketMessage;
129    /// use log::info;
130    ///
131    /// #[tokio::main]
132    /// async fn main() {
133    ///     let client: Client = unimplemented!("See other examples");
134    ///
135    ///     client.on_message_simple(move |message| {
136    ///         match message {
137    ///             WebsocketMessage::OutgoingMessage(sms) => info!("Outgoing message: {:?}", sms),
138    ///             _ => { }
139    ///         }
140    ///     }).await?
141    /// }
142    /// ```
143    #[cfg(feature = "websocket")]
144    pub async fn on_message_simple<F>(&self, callback: F) -> ClientResult<()>
145    where
146        F: Fn(ws::types::WebsocketMessage) + Send + Sync + 'static,
147    {
148        let ws_client = self.ws_client
149            .as_ref()
150            .ok_or(ClientError::ConfigError("WebSocketClient"))?;
151
152        let mut ws_guard = ws_client.lock().await;
153        ws_guard.on_message(callback);
154
155        Ok(())
156    }
157
158    /// Start the WebSocket connection.
159    #[cfg(feature = "websocket")]
160    pub async fn start_background_websocket(&self) -> ClientResult<()> {
161        let ws_client = self.ws_client
162            .as_ref()
163            .ok_or(ClientError::ConfigError("WebsocketConfig"))?;
164
165        let mut ws_guard = ws_client.lock().await;
166        ws_guard.start_background().await?;
167
168        Ok(())
169    }
170
171    /// Start the WebSocket connection and block until closed.
172    #[cfg(feature = "websocket")]
173    pub async fn start_blocking_websocket(&self) -> ClientResult<()> {
174        let ws_client = self.ws_client
175            .as_ref()
176            .ok_or(ClientError::ConfigError("WebsocketConfig"))?;
177
178        let mut ws_guard = ws_client.lock().await;
179        ws_guard.start_blocking().await?;
180
181        Ok(())
182    }
183
184    /// Stop the WebSocket connection.
185    #[cfg(feature = "websocket")]
186    pub async fn stop_background_websocket(&self) -> ClientResult<()> {
187        let ws_client = self.ws_client
188            .as_ref()
189            .ok_or(ClientError::ConfigError("WebsocketConfig"))?;
190
191        let mut ws_guard = ws_client.lock().await;
192        ws_guard.stop_background().await?;
193
194        Ok(())
195    }
196
197    /// Check if the WebSocket is currently connected.
198    #[cfg(feature = "websocket")]
199    pub async fn is_websocket_connected(&self) -> bool {
200        let Some(ws_client) = &self.ws_client else {
201            return false;
202        };
203
204        let ws_guard = ws_client.lock().await;
205        ws_guard.is_connected().await
206    }
207
208    /// Force a WebSocket reconnection.
209    #[cfg(feature = "websocket")]
210    pub async fn reconnect_websocket(&self) -> ClientResult<()> {
211        let ws_client = self.ws_client
212            .as_ref()
213            .ok_or(ClientError::NoWebsocketClient)?;
214
215        let ws_guard = ws_client.lock().await;
216        ws_guard.reconnect().await.map_err(ClientError::from)
217    }
218}
219impl Drop for Client {
220    fn drop(&mut self) {
221        // The WebSocket client will handle its own cleanup in its Drop impl
222        // This is just here to ensure proper cleanup ordering.
223    }
224}