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}