simploxide_client/
lib.rs

1//! For first-time users it's recommended to get hands-on experience by running some example bots
2//! on [GitHub](https://github.com/a1akris/simploxide/tree/main/simploxide-client) before writing
3//! their own.
4//!
5//! # How to write a SimpleX bot?
6//!
7//! First of all, you **must** use a tokio runtime. The current `simploxide` implementation heavily
8//! depends on it.
9//!
10//! It's also recommended to use `simploxide_client::prelude::*` everywhere to not pollute the
11//! import section.
12//!
13//! ##### Now to the bot
14//!
15//! The most common bot structure will look like this:
16//!
17//! 1. Initialize a web socket connection with the simplex-chat daemon(you can run simplex-chat as
18//!    a daemon using the `simplex-chat -p <port>` command)
19//! 1. Prequery some info and do some validations required for your bot to work: this typically
20//!    includes creating the bot address, switching to the right bot user, etc
21//! 1. Start an event reactor loop and process the events.
22//!
23//! Example:
24//!
25//! ```ignore
26//! use simploxide_client::prelude::*;
27//! use futures::stream::TryStreamExt;
28//!
29//! #[tokio::main]
30//! async fn main() -> Result<(), Box<dyn Error>> {
31//!     // Init websocket connection with SimpleX daemon
32//!     let (client, mut events) = simploxide_client::connect("ws://127.0.0.1:5225").await?;
33//!
34//!     // Pre-query and validate stuff
35//!     client.do_some_initialization().await?;
36//!
37//!
38//!     // Implement event reactor
39//!     while let Some(ev) = events.try_next().await? {
40//!         match ev {
41//!             Event::SomeEvent1(SomeEvent1 { data }) => {
42//!                 client.process_event1(data).await?;
43//!             }
44//!             Event::SomeEvent2(SomeEvent2 { data }) => {
45//!                 client.process_event2(data).await?;
46//!                 break;
47//!             }
48//!             _ => (), // Ignore events you're not interested in.
49//!         }
50//!     }
51//!
52//!
53//!     // (Optional) some cleanup
54//!
55//!
56//!     Ok(())
57//!
58//! }
59//! ```
60//!
61//! Note that the reactor part in the example above is very inefficient because it reacts on events
62//! sequentially - not processing any events until the client responds to the current event. This
63//! can be OK if your bot doesn't need to operate under a heavy-load, such reactor could also be
64//! useful during the development because it is trivial to debug, but for deployment it is
65//! advisable to enable full asynchronous multi-threaded event processing which can be simply
66//! achieved by moving event handlers into tokio tasks:
67//!
68//!
69//!```ignore
70//!     // Implement event reactor
71//!     while let Some(ev) = events.try_next().await? {
72//!         let client = client.clone();
73//!         match ev {
74//!             Event::SomeEvent1(SomeEvent1 { data }) => {
75//!                 tokio::spawn(async move {
76//!                     client.process_event1(data).await?;
77//!                 });
78//!             }
79//!             Event::SomeEvent2(SomeEvent2 { data }) => {
80//!                 tokio::spawn(async move {
81//!                     client.process_event2(data).await?;
82//!                     client.disconnect();
83//!                 });
84//!             }
85//!             _ => (), // Ignore events you're not interested in.
86//!         }
87//!     }
88//!```
89//!
90//! Now the event loop can't be terimnated with a `break` statetement because events are
91//! processed asynchronously in their own tasks. You can call `client.disconnect()` in this case to
92//! initiate a graceful shutdown which will eventually end the event stream, or you can use a
93//! cancellation token + tokio::select! and break the loop when the token is triggered.
94//!
95//! ##### Trivial use-cases
96//!
97//! Some applications may not need to react to events, they can act like scripts or like remote
98//! controllers for the SimpleX chat instance. In this case, drop the event stream immediately to
99//! prevent events from buffering and leaking memory:
100//!
101//!
102//! ```ignore
103//!     // Init websocket connection with SimpleX daemon
104//!     let (client, events) = simploxide_client::connect("ws://127.0.0.1:5225").await?;
105//!     drop(events);
106//! ```
107//!
108//!
109//! ##### More complicated use-cases
110//!
111//! Some applications may have several event loops, so the reactor could be moved into a separate
112//! async task. In this case it's recommended to save the handle of the tokio task and await it
113//! before the program exits to prevent data losses.
114//!
115//! ```ignore
116//!     // Init websocket connection with SimpleX daemon
117//!     let (client, events) = simploxide_client::connect("ws://127.0.0.1:5225").await?;
118//!     let handle = tokio::spawn(event_reactor(events));
119//!
120//!
121//!     //..
122//!
123//!     handle.await
124//! ```
125//!
126//!
127//! ##### Graceful shutdown guarantees
128//!
129//! When calling `client.disconnect()` it's guaranteed that all futures created before this call
130//! will still receive their responses and that all futures created after this call will resolve
131//! with `tungstenite::Error::AlreadyClosed`.
132//!
133//! Note however, that if your task sends multiple requests and you're calling
134//! `client.disconnect()` from another task then it's not guaranteed that your task will get all
135//! responses. In fact any future can resolve with an error:
136//!
137//! ```ignore
138//! async fn my_handler(client: simploxide_client::Client) -> HandlerResult {
139//!     let res1 = client.req1().await?;
140//!     // <--------------------------------- Disconnect triggers at this point
141//!     let res2 = client.req2(res1).await?; // This future will throw an error
142//!     Ok(res2)
143//! }
144//! ```
145//!
146//! You will need to implement additional synchronization mechanisms if you want to ensure that all
147//! handlers run to completion when client disconnects.
148//!
149//! To understand more about the client implementation read the [`core`] docs.
150//!
151//! # How to work with this documentation?
152//!
153//! The [`Client`] page should become your main page and the [`events`] page should become your
154//! secondary page. From these 2 pages you can reach all corners of the docs in a structured
155//! manner.
156//!
157use futures::Stream;
158use simploxide_api_types::{
159    JsonObject,
160    client_api::{BadResponseError, ClientApiError},
161    events::Event,
162};
163use simploxide_core::{EventQueue, EventReceiver, RawClient};
164use std::{sync::Arc, task};
165
166pub use simploxide_api_types::{
167    self as types,
168    client_api::{self, ClientApi},
169    commands, events, responses,
170    utils::CommandSyntax,
171};
172pub use simploxide_core::{
173    self as core, Error as CoreError, Result as CoreResult, tungstenite::Error as WsError,
174};
175
176pub mod prelude;
177
178pub type ClientResult<T = ()> = std::result::Result<T, ClientError>;
179
180/// A wrapper over [`simploxide_core::connect`] that turns [`simploxide_core::RawClient`] into
181/// [`Client`] and raw event queue into the [`EventStream`] which handle serialization/deserialization.
182///
183/// ```ignore
184/// let (client, mut events) = simploxide_client::connect("ws://127.0.0.1:5225").await?;
185///
186/// let current_user = client.api_show_active_user().await?;
187/// println!("{current_user:#?}");
188///
189/// while let Some(ev) = events.try_next().await? {
190///     // Process events...
191/// }
192/// ```
193pub async fn connect<S: AsRef<str>>(uri: S) -> Result<(Client, EventStream), WsError> {
194    let (raw_client, raw_event_queue) = simploxide_core::connect(uri.as_ref()).await?;
195    Ok((Client::from(raw_client), EventStream::from(raw_event_queue)))
196}
197
198/// Like [`connect`] but retries to connect `retries_count` times before returning an error. This
199/// method is needed when you run simplex-cli programmatically and don't know when WebSocket port
200/// becomes available.
201///
202/// ```ignore
203/// let port = 5225;
204/// let cli = SimplexCli::spawn(port);
205/// let uri = format!("ws://127.0.0.1:{port}");
206///
207/// let (client, mut events) = simploxide_client::retry_connect(&uri, Duration::from_secs(1), 10).await?;
208///
209/// //...
210///
211/// ```
212pub async fn retry_connect<S: AsRef<str>>(
213    uri: S,
214    retry_delay: std::time::Duration,
215    mut retries_count: usize,
216) -> Result<(Client, EventStream), WsError> {
217    loop {
218        match connect(uri.as_ref()).await {
219            Ok(connection) => break Ok(connection),
220            Err(e) if retries_count == 0 => break Err(e),
221            Err(_) => {
222                retries_count -= 1;
223                tokio::time::sleep(retry_delay).await
224            }
225        }
226    }
227}
228
229pub struct EventStream(EventReceiver);
230
231impl From<EventQueue> for EventStream {
232    fn from(value: EventQueue) -> Self {
233        Self(value.into_receiver())
234    }
235}
236
237impl Stream for EventStream {
238    type Item = CoreResult<Arc<Event>>;
239
240    fn poll_next(
241        mut self: std::pin::Pin<&mut Self>,
242        cx: &mut task::Context<'_>,
243    ) -> task::Poll<Option<Self::Item>> {
244        self.0.poll_recv(cx).map(|opt| {
245            opt.map(|res| res.map(|ev| serde_json::from_value::<Arc<Event>>(ev).unwrap()))
246        })
247    }
248}
249
250/// A high level SimpleX-Chat client which provides typed API methods with automatic command
251/// serialization and response deserialization.
252#[derive(Clone)]
253pub struct Client {
254    inner: RawClient,
255}
256
257impl From<RawClient> for Client {
258    fn from(inner: RawClient) -> Self {
259        Self { inner }
260    }
261}
262
263impl Client {
264    /// Initiates a graceful shutdown for the underlying web socket connection. See
265    /// [`simploxide_core::RawClient::disconnect`] for details.
266    pub fn disconnect(self) {
267        self.inner.disconnect();
268    }
269}
270
271impl ClientApi for Client {
272    type Error = ClientError;
273
274    async fn send_raw(&self, command: String) -> Result<JsonObject, Self::Error> {
275        self.inner
276            .send(command)
277            .await
278            .map_err(ClientError::WebSocketFailure)
279    }
280}
281
282/// See [`core::client_api::AllowUndocumentedResponses`] if you don't want to trigger an error when
283/// you receive undocumeted responses(you usually receive undocumented responses when your
284/// simplex-chat server version is not compatible with the simploxide-client version. Keep an eye
285/// on the
286/// [Version compatability table](https://github.com/a1akris/simploxide?tab=readme-ov-file#version-compatability-table)
287/// )
288#[derive(Debug)]
289pub enum ClientError {
290    /// Critical error signalling that the web socket connection is dropped for some reason. You
291    /// will have to reconnect to the SimpleX server to recover from this one.
292    WebSocketFailure(CoreError),
293    /// SimpleX command error or unexpected(undocumented) response.
294    BadResponse(BadResponseError),
295}
296
297impl std::error::Error for ClientError {}
298
299impl std::fmt::Display for ClientError {
300    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
301        match self {
302            ClientError::WebSocketFailure(err) => writeln!(f, "Web socket failure: {err}"),
303            ClientError::BadResponse(err) => err.fmt(f),
304        }
305    }
306}
307
308impl From<BadResponseError> for ClientError {
309    fn from(err: BadResponseError) -> Self {
310        Self::BadResponse(err)
311    }
312}
313
314impl ClientApiError for ClientError {
315    fn bad_response_mut(&mut self) -> Option<&mut BadResponseError> {
316        if let Self::BadResponse(resp) = self {
317            Some(resp)
318        } else {
319            None
320        }
321    }
322}