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//! Secondly, it's recommended to use `simploxide_client::prelude::*` if you don't want your import
11//! section to explode. The prelude reexports all top-level types required for sending requests,
12//! destructuring responses and matching events, you'll still need to manually import intermediary
13//! types and there are a lot of them, the prelude just greately reduces the amount of the same
14//! imports per file.
15//!
16//! ##### Now to the bot
17//!
18//! The most common bot structure will look like this:
19//!
20//!
21//! ```ignore
22//! use simploxide_client::prelude::*;
23//! use futures::stream::TryStreamExt;
24//!
25//! #[tokio::main]
26//! async fn main() -> Result<(), Box<dyn Error>> {
27//!     // Init websocket connection with SimpleX daemon
28//!     let (client, mut events) = simploxide_client::connect("ws://127.0.0.1:5225").await?;
29//!
30//!     // Pre-query and validate stuff
31//!     client.do_some_initialization().await?;
32//!
33//!
34//!     // Implement event reactor
35//!     while let Some(ev) = events.try_next().await? {
36//!         match ev {
37//!             Event::SomeEvent1(SomeEvent1 { data }) => {
38//!                 client.process_event1(data).await?;
39//!             }
40//!             Event::SomeEvent2(SomeEvent2 { data }) => {
41//!                 client.process_event2(data).await?;
42//!                 break;
43//!             }
44//!             _ => (), // Ignore events you're not interested in.
45//!         }
46//!     }
47//!
48//!
49//!     // (Optional) some cleanup
50//!
51//!
52//!     Ok(())
53//!
54//! }
55//! ```
56//!
57//! 1. Initialize a web socket connection with the simplex-chat daemon. You can run simplex-chat as
58//!    a daemon with `simplex-chat -p <port>` command.
59//! 1. Prequery some info and do some validations required for your bot to work: this typically
60//!    includes getting or creating the bot address, switching to the right bot user, etc
61//! 1. Start an event reactor loop and process the events.
62//!
63//! Everything looks simple and trivial but the reactor part in the example above is terribly
64//! inefficient. It reacts on events sequentially waiting for client to respond to the first event
65//! before processing the second. This can be fine if your bot doesn't need to operate under a
66//! heavy-load, such reactor would also be useful during the development because it is trivial to
67//! debug however, for production it's advisable to enable full asynchronous multi-threaded
68//! processing that can be achieved by simply moving the event handlers into tokio tasks:
69//!
70//!
71//!```ignore
72//!     // Implement event reactor
73//!     while let Some(ev) = events.try_next().await? {
74//!         let client = client.clone();
75//!         match ev {
76//!             Event::SomeEvent1(SomeEvent1 { data }) => {
77//!                 tokio::spawn(async move {
78//!                     client.process_event1(data).await?;
79//!                 });
80//!             }
81//!             Event::SomeEvent2(SomeEvent2 { data }) => {
82//!                 tokio::spawn(async move {
83//!                     client.process_event2(data).await?;
84//!                     client.disconnect();
85//!                 });
86//!             }
87//!             _ => (), // Ignore events you're not interested in.
88//!         }
89//!     }
90//!```
91//!
92//! Note, that we can't terminate the event loop with a `break` statetement because the event is
93//! being processed asynchronously in its own task. You can call `client.disconnect()` in this case
94//! to initiate a graceful shutdown which will eventually end the event stream, but even with
95//! strong guarantees the graceful shutdown provides it cannot guarantee that events, which
96//! occurred before the shutdown, will be processed to completion as tasks may need to send several
97//! requests to complete successfully, so if this is important for you application to process
98//! events atomically you should use primitives like tokio channels and notifies to break the loop
99//! without dropping the web socket connection.
100//!
101//!
102//! ##### A simpler use case
103//!
104//! Some applications may not need to react on events, they can act like scripts, or like remote
105//! controllers for a SimpleX chat instance. In this case, drop the event stream immediately to
106//! prevent events from buffering and leaking memory:
107//!
108//!
109//! ```ignore
110//!     // Init websocket connection with SimpleX daemon
111//!     let (client, events) = simploxide_client::connect("ws://127.0.0.1:5225").await?;
112//!     drop(events);
113//! ```
114//!
115//!
116//! ##### More complicated use case
117//!
118//! Some applications may have several event loops, so the reactor could be moved into a separate
119//! async task. In this case it's recommended to save the handle of the tokio task and await it
120//! before the program exits to prevent data losses(e.g. to ensure that client.disconnect() is called).
121//!
122//! ```ignore
123//!     // Init websocket connection with SimpleX daemon
124//!     let (client, events) = simploxide_client::connect("ws://127.0.0.1:5225").await?;
125//!     let handle = tokio::spawn(event_reactor(events));
126//!
127//!
128//!     //..
129//!
130//!     handle.await
131//! ```
132//!
133//! # How to work with this documentation?
134//!
135//! The [`Client`] page should become your main page. From there you can reach the deepest corners
136//! of the docs in a structured manner. Looking at other modules is not very helpful unless you're
137//! looking for something specific.
138//!
139//! If you need to understand how async is being implemented in the client check out the [`core`]
140//! docs.
141//!
142use futures::{Stream, TryStreamExt as _};
143use simploxide_api_types::{JsonObject, events::Event};
144use simploxide_core::RawClient;
145use std::sync::Arc;
146use tokio_stream::wrappers::UnboundedReceiverStream;
147
148pub use simploxide_api_types::{
149    self as types, client_api::ClientApi, commands, events, responses, utils::CommandSyntax,
150};
151pub use simploxide_core::{
152    self as core, Error as CoreError, Result as CoreResult, tungstenite::Error as WsError,
153};
154
155pub mod prelude;
156
157/// A wrapper over [`simploxide_core::connect`] that turns [`simploxide_core::RawClient`] into
158/// [`Client`] and the event queue into the event stream with automatic event
159/// deserialization.
160///
161/// ```ignore
162/// let (client, mut events) = simploxide_client::connect("ws://127.0.0.1:5225").await?;
163///
164/// let current_user  = client.api_show_active_user().await?;
165/// println!("{current_user:#?}");
166///
167/// while let Some(ev) = events.try_next().await? {
168///     // Process events...
169/// }
170/// ```
171pub async fn connect<S: AsRef<str>>(
172    uri: S,
173) -> Result<
174    (
175        Client,
176        impl Stream<Item = Result<Arc<Event>, CoreError>> + Unpin,
177    ),
178    WsError,
179> {
180    let (inner, raw_queue) = simploxide_core::connect(uri.as_ref()).await?;
181    let stream = UnboundedReceiverStream::new(raw_queue.into_receiver());
182
183    Ok((
184        Client { inner },
185        stream.map_ok(|ev| serde_json::from_value::<Arc<Event>>(ev).unwrap()),
186    ))
187}
188
189/// A high level simplex client that implements [`ClientApi`] which provides typed client
190/// methods with automatic command serialization/response deserialization.
191pub struct Client {
192    inner: RawClient,
193}
194
195impl Client {
196    /// Initiates a graceful shutdown for the underlying web socket connection. See
197    /// [`simploxide_core::RawClient::disconnect`] for details.
198    pub fn disconnect(self) {
199        self.inner.disconnect();
200    }
201}
202
203impl ClientApi for Client {
204    type Error = CoreError;
205
206    async fn send_raw(&self, command: String) -> Result<JsonObject, Self::Error> {
207        self.inner.send(command).await
208    }
209}