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