Skip to main content

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#![allow(clippy::missing_errors_doc)]
8
9use crate::error::{ClientError, ClientResult};
10pub use sms_types as types;
11
12pub mod config;
13pub mod error;
14
15#[cfg(feature = "http")]
16pub mod http;
17
18#[cfg(feature = "websocket")]
19pub mod ws;
20
21/// SMS Client.
22#[derive(Clone, Debug)]
23pub struct Client {
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    /// Create an SMS client with a connection config.
32    pub fn new(config: config::ClientConfig) -> ClientResult<Self> {
33        let tls = config.tls;
34
35        #[cfg(feature = "http")]
36        let http_client = if let Some(http_config) = config.http {
37            Some(std::sync::Arc::new(http::HttpClient::new(
38                http_config,
39                tls.as_ref(),
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(ws::WebSocketClient::new(
48                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(std::convert::AsRef::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::Client;
85    /// use sms_client::types::events::Event;
86    /// use sms_client::ws::events::WebsocketEvent;
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    ///             WebsocketEvent::Server(Event::IncomingMessage(sms)) => {
96    ///                 // Can access client.http() here!
97    ///             },
98    ///             WebsocketEvent::Reconnection(reconnection) => {
99    ///                 // The websocket connection state has been updated!
100    ///             },
101    ///             _ => { }
102    ///         }
103    ///     }).await?
104    /// }
105    /// ```
106    #[cfg(feature = "websocket")]
107    pub async fn on_message<F>(&self, callback: F) -> ClientResult<()>
108    where
109        F: Fn(ws::events::WebsocketEvent, std::sync::Arc<Self>) + Send + Sync + 'static,
110    {
111        let ws_client = self
112            .ws_client
113            .as_ref()
114            .ok_or(ClientError::ConfigError("WebSocketClient"))?;
115
116        let mut ws_guard = ws_client.lock().await;
117        let client_arc = std::sync::Arc::new(self.clone());
118
119        ws_guard.on_message(move |msg| callback(msg, std::sync::Arc::clone(&client_arc)));
120
121        Ok(())
122    }
123
124    /// Set the callback for incoming WebSocket messages (simple version without client copy).
125    /// This must be called before starting the WebSocket connection.
126    ///
127    /// # Example
128    /// ```
129    /// use sms_client::Client;
130    /// use sms_client::types::events::Event;
131    /// use sms_client::ws::events::WebsocketEvent;
132    /// use log::info;
133    ///
134    /// #[tokio::main]
135    /// async fn main() {
136    /// let client: Client = unimplemented!("See other examples");
137    ///
138    ///     client.on_message_simple(move |message| {
139    ///         match message {
140    ///             WebsocketEvent::Server(Event::IncomingMessage(sms)) => info!("Outgoing message: {:?}", sms),
141    ///             _ => { }
142    ///         }
143    ///     }).await?
144    /// }
145    /// ```
146    #[cfg(feature = "websocket")]
147    pub async fn on_message_simple<F>(&self, callback: F) -> ClientResult<()>
148    where
149        F: Fn(ws::events::WebsocketEvent) + Send + Sync + 'static,
150    {
151        let ws_client = self
152            .ws_client
153            .as_ref()
154            .ok_or(ClientError::ConfigError("WebSocketClient"))?;
155
156        let mut ws_guard = ws_client.lock().await;
157        ws_guard.on_message(callback);
158
159        Ok(())
160    }
161
162    /// Start the WebSocket connection.
163    #[cfg(feature = "websocket")]
164    pub async fn start_background_websocket(&self) -> ClientResult<()> {
165        let ws_client = self
166            .ws_client
167            .as_ref()
168            .ok_or(ClientError::ConfigError("WebsocketConfig"))?;
169
170        let mut ws_guard = ws_client.lock().await;
171        ws_guard.start_background().await?;
172
173        Ok(())
174    }
175
176    /// Start the WebSocket connection and block until closed.
177    #[cfg(feature = "websocket")]
178    pub async fn start_blocking_websocket(&self) -> ClientResult<()> {
179        let ws_client = self
180            .ws_client
181            .as_ref()
182            .ok_or(ClientError::ConfigError("WebsocketConfig"))?;
183
184        let mut ws_guard = ws_client.lock().await;
185        ws_guard.start_blocking().await?;
186
187        Ok(())
188    }
189
190    /// Stop the WebSocket connection.
191    #[cfg(feature = "websocket")]
192    pub async fn stop_background_websocket(&self) -> ClientResult<()> {
193        let ws_client = self
194            .ws_client
195            .as_ref()
196            .ok_or(ClientError::ConfigError("WebsocketConfig"))?;
197
198        let mut ws_guard = ws_client.lock().await;
199        ws_guard.stop_background().await?;
200
201        Ok(())
202    }
203
204    /// Check if the WebSocket is currently connected.
205    #[cfg(feature = "websocket")]
206    pub async fn is_websocket_connected(&self) -> bool {
207        let Some(ws_client) = &self.ws_client else {
208            return false;
209        };
210
211        let ws_guard = ws_client.lock().await;
212        ws_guard.is_connected().await
213    }
214
215    /// Force a WebSocket reconnection.
216    #[cfg(feature = "websocket")]
217    pub async fn reconnect_websocket(&self) -> ClientResult<()> {
218        let ws_client = self
219            .ws_client
220            .as_ref()
221            .ok_or(ClientError::NoWebsocketClient)?;
222
223        let ws_guard = ws_client.lock().await;
224        ws_guard.reconnect().await.map_err(ClientError::from)
225    }
226}
227impl Drop for Client {
228    fn drop(&mut self) {
229        // The WebSocket client will handle its own cleanup in its Drop impl
230        // This is just here to ensure proper cleanup ordering.
231    }
232}