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//! # How to work with this documentation?
130//!
131//! The [`Client`] page should become your main page. From there you can reach the deepest corners
132//! of the docs in a structured manner. Looking at other modules is not very helpful unless you're
133//! looking for something specific.
134//!
135//! If you need to understand how async is being implemented in the client check out the [`core`]
136//! docs.
137//!
138use futures::{Stream, TryStreamExt as _};
139use simploxide_api_types::{JsonObject, events::Event};
140use simploxide_core::RawClient;
141use tokio_stream::wrappers::UnboundedReceiverStream;
142
143pub use simploxide_api_types::{
144 self as types, client_api::ClientApi, commands, events, responses, utils::CommandSyntax,
145};
146pub use simploxide_core::{
147 self as core, Error as CoreError, Result as CoreResult, tungstenite::Error as WsError,
148};
149
150pub mod prelude;
151
152/// A wrapper over [`simploxide_core::connect`] that turns [`simploxide_core::RawClient`] into
153/// [`Client`] and the event queue into the event stream with automatic event
154/// deserialization.
155///
156/// ```ignore
157/// let (client, mut events) = simploxide_client::connect("ws://127.0.0.1:5225").await?;
158///
159/// let current_user = client.api_show_active_user().await?;
160/// println!("{current_user:#?}");
161///
162/// while let Some(ev) = events.try_next().await? {
163/// // Process events...
164/// }
165/// ```
166pub async fn connect<S: AsRef<str>>(
167 uri: S,
168) -> Result<(Client, impl Stream<Item = Result<Event, CoreError>> + Unpin), WsError> {
169 let (inner, raw_queue) = simploxide_core::connect(uri.as_ref()).await?;
170 let stream = UnboundedReceiverStream::new(raw_queue.into_receiver());
171
172 Ok((
173 Client { inner },
174 stream.map_ok(|ev| serde_json::from_value::<Event>(ev).unwrap()),
175 ))
176}
177
178/// A high level simplex client that implements [`ClientApi`] which provides typed client
179/// methods with automatic command serialization/response deserialization.
180pub struct Client {
181 inner: RawClient,
182}
183
184impl Client {
185 /// Initiates a graceful shutdown for the underlying web socket connection. See
186 /// [`simploxide_core::RawClient::disconnect`] for details.
187 pub fn disconnect(self) {
188 self.inner.disconnect();
189 }
190}
191
192impl ClientApi for Client {
193 type Error = CoreError;
194
195 async fn send_raw(&self, command: String) -> Result<JsonObject, Self::Error> {
196 self.inner.send(command).await
197 }
198}