simploxide_client/
lib.rs

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