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;
141use simploxide_api_types::{JsonObject, events::Event};
142use simploxide_core::{EventQueue, EventReceiver, RawClient};
143use std::{sync::Arc, task};
144
145pub use simploxide_api_types::{
146    self as types, client_api::ClientApi, commands, events, responses, utils::CommandSyntax,
147};
148pub use simploxide_core::{
149    self as core, Error as CoreError, Result as CoreResult, tungstenite::Error as WsError,
150};
151
152pub mod prelude;
153
154/// A wrapper over [`simploxide_core::connect`] that turns [`simploxide_core::RawClient`] into
155/// [`Client`] and the event queue into the event stream with automatic event
156/// deserialization.
157///
158/// ```ignore
159/// let (client, mut events) = simploxide_client::connect("ws://127.0.0.1:5225").await?;
160///
161/// let current_user  = client.api_show_active_user().await?;
162/// println!("{current_user:#?}");
163///
164/// while let Some(ev) = events.try_next().await? {
165///     // Process events...
166/// }
167/// ```
168pub async fn connect<S: AsRef<str>>(uri: S) -> Result<(Client, EventStream), WsError> {
169    let (raw_client, raw_event_queue) = simploxide_core::connect(uri.as_ref()).await?;
170
171    Ok((Client::from(raw_client), EventStream::from(raw_event_queue)))
172}
173
174pub struct EventStream(EventReceiver);
175
176impl From<EventQueue> for EventStream {
177    fn from(value: EventQueue) -> Self {
178        Self(value.into_receiver())
179    }
180}
181
182impl Stream for EventStream {
183    type Item = CoreResult<Arc<Event>>;
184
185    fn poll_next(
186        mut self: std::pin::Pin<&mut Self>,
187        cx: &mut task::Context<'_>,
188    ) -> task::Poll<Option<Self::Item>> {
189        self.0.poll_recv(cx).map(|opt| {
190            opt.map(|res| res.map(|ev| serde_json::from_value::<Arc<Event>>(ev).unwrap()))
191        })
192    }
193}
194
195/// A high level SimpleX-Chat client which provides typed API methods with automatic command
196/// serialization/response deserialization.
197#[derive(Clone)]
198pub struct Client {
199    inner: RawClient,
200}
201
202impl From<RawClient> for Client {
203    fn from(inner: RawClient) -> Self {
204        Self { inner }
205    }
206}
207
208impl Client {
209    /// Initiates a graceful shutdown for the underlying web socket connection. See
210    /// [`simploxide_core::RawClient::disconnect`] for details.
211    pub fn disconnect(self) {
212        self.inner.disconnect();
213    }
214}
215
216impl ClientApi for Client {
217    type Error = CoreError;
218
219    async fn send_raw(&self, command: String) -> Result<JsonObject, Self::Error> {
220        self.inner.send(command).await
221    }
222}