Skip to main content

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//! This SDK is intended to be used with the `tokio` runtime. Here are the steps to implement any bot:
6//!
7//! ### 1. Choose a backend
8//!
9//! `simploxide` supports both **WebSocket** and **FFI** SimpleX-Chat backends.
10//! All FFI-exclusive methods are reimplemented in native Rust, so in practice the backends differ
11//! only in their runtime characteristics: a single-process app via **FFI** vs. an app that
12//! connects to a running SimpleX-Chat **WebSocket** server.
13//!
14//! Since both backends are equally capable, always start development with the **WebSocket** backend
15//! (enabled by default). Switching to **FFI** later is as simple as replacing `ws` imports with
16//! `ffi` imports, but **FFI** requires configuring the crate build and obliges you to use the
17//! AGPL-3.0 license. You can read more about switching to **FFI** [here](simploxide-sxcrt-sys).
18//!
19//! ### 2. Initialise the bot
20//!
21//! `simploxide` provides convenient bot builders to launch and configure your bot.
22//!
23//! ```ignore
24//! let (bot, events, mut cli) = ws::BotBuilder::new("YesMan", 5225)
25//!     .db_prefix("db/bot")
26//!     // Create a public bot address that auto-accepts new users with a welcome message.
27//!     .auto_accept_with(
28//!         "Hello, I'm a bot that always agrees with my users",
29//!     )
30//!     // Launch the CLI, connect the client, and initialise the bot.
31//!     .launch()
32//!     .await?;
33//!
34//! let address = bot.address().await?;
35//! println!("My address: {address}");
36//! ```
37//!
38//! See all available options in [ws::BotBuilder] and [ffi::BotBuilder].
39//!
40//! ### 3. Set up an event dispatcher
41//!
42//! Dispatchers are zero-cost and provide a convenient API for handling events.
43//!
44//! ```ignore
45//! // into_dispatcher accepts any type and creates a dispatcher from the event stream.
46//! // The value provided here is passed into all event handlers as a second argument.
47//! events.into_dispatcher(bot)
48//!     .on(new_messages)
49//!     .dispatch()
50//!     .await?;
51//! ```
52//!
53//! Learn more about dispatchers in the [dispatcher] and [EventStream] docs.
54//!
55//! ### 4. Implement event handlers
56//!
57//! The first handler argument determines which event the handler processes. The [StreamEvents]
58//! type allows interrupting event dispatching via [`StreamEvents::Break`].
59//!
60//! ```ignore
61//! async fn new_msgs(ev: Arc<NewChatItems>, bot: Bot) -> ws::ClientResult<StreamEvents> {
62//!     for (chat, msg, content) in ev.filter_messages() {
63//!         bot.update_msg_reaction(chat, msg, Reaction::Set("👍")).await?;
64//!
65//!         bot.send_msg(chat, "I absolutely agree with this!".bold())
66//!            .reply_to(msg)
67//!            .await?;
68//!     }
69//!
70//!     Ok(StreamEvents::Continue)
71//! }
72//! ```
73//!
74//! Message builders are quite powerful, see [`messages`] for details. In most places where an
75//! ID is expected you can pass a struct directly; see the type-safe conversions available in [id].
76//!
77//! ### 5. Execute cleanup before exiting
78//!
79//! ```ignore
80//! bot.shutdown().await;
81//! cli.kill().await?;
82//! ```
83//!
84//! ## Features
85//!
86//! - **`cli`** *(default)*: WebSocket backend ([`ws`]) with a built-in runner that spawns and
87//!   manages a local `simplex-chat` process. Use [`ws::BotBuilder::launch`] to start everything
88//!   in one call.
89//! - **`websocket`**: WebSocket backend ([`ws`]) without the CLI runner. Use
90//!   [`ws::BotBuilder::connect`] to attach to an already-running `simplex-chat` server.
91//! - **`ffi`**: FFI backend ([`ffi`]) that embeds the SimpleX-Chat library in-process.
92//!   Requires AGPL-3.0 and additional build configuration; see `simploxide-sxcrt-sys`.
93//! - **`native_crypto`**: Native Rust implementation of client-side encryption(XSalsa20 + Poly1305). Enables
94//!   [`ImagePreview::from_crypto_file`](preview::ImagePreview::from_crypto_file) and [crypto::fs]
95//!   module allowing to encrypt decrypt files directly in the Rust code
96//! - **`multimedia`**: Image transcoding via the `image` crate. Enables
97//!   [`preview::transcoder::Transcoder`] and automatic thumbnail generation for [`messages::Image`].
98//!   [`preview::ImagePreview`] automatically tries to transcode its sources to JPEGs with this
99//!   feature on
100//! - **`xftp`**: XFTP file transfer support. Enables [`xftp::XftpClient`], which intercepts
101//!   streamlines file downlaods with a `download_file` method.
102//! - **`cancellation`**: Re-exports [`tokio_util::sync::CancellationToken`] and enables helper
103//!   methods for cooperative shutdown.
104//! - **`crypto`**: Low-level cryptographic primitives (zeroize, rand). Pulled in automatically by
105//!   `native_crypto`. Useful on its own if you wish to use your own crypto implementation.
106//! - **`fullcli`**: Convenience bundle: `cli` + `native_crypto` + `multimedia` + `xftp` +
107//!   `cancellation`.
108//! - **`fullffi`**: Convenience bundle: `ffi` + `native_crypto` + `multimedia` + `xftp` +
109//!   `cancellation`.
110//!
111//! ### How to work with this documentation?
112//!
113//! The [bot] page should be your primary reference and the [events] page your secondary one.
114//! From these two pages you should be able to find everything in a structured manner.
115
116#[cfg(feature = "crypto")]
117pub mod crypto;
118#[cfg(feature = "ffi")]
119pub mod ffi;
120#[cfg(feature = "websocket")]
121pub mod ws;
122#[cfg(feature = "xftp")]
123pub mod xftp;
124
125pub mod bot;
126pub mod dispatcher;
127pub mod ext;
128pub mod id;
129pub mod messages;
130pub mod prelude;
131pub mod preview;
132
133mod util;
134
135pub use simploxide_api_types::{
136    self as types,
137    client_api::{self, BadResponseError, ClientApi, ClientApiError},
138    commands, events,
139    events::{Event, EventKind},
140    responses,
141    utils::CommandSyntax,
142};
143
144#[cfg(feature = "cancellation")]
145pub use tokio_util::{self, sync::CancellationToken};
146
147pub use dispatcher::DispatchChain;
148
149use futures::{Stream, TryStreamExt as _};
150
151use std::{
152    pin::Pin,
153    sync::Arc,
154    task::{Context, Poll},
155};
156
157/// The high level event stream that embeds event filtering.
158///
159/// Parsing SimpleX events may be costly, they are quite large deeply nested structs with a lot of
160/// [`String`] and [`std::collections::BTreeMap`] types. This stream provides filtering APIs
161/// allowing to parse and propagate events the application handles and drop all other events early
162/// without allocating any extra memory.
163///
164/// By default filters are disabled and no events are dropped. Use [`Self::set_filter`] to only
165/// receive events you're interested in.
166///
167/// Use [`Self::into_dispatcher`] to handle events conveniently. Dispatchers are completely
168/// zerocost, manage filters internally, and provide a high-level easy to use API covering the
169/// absolute majority of use cases.
170pub struct EventStream<P> {
171    filter: [bool; EventKind::COUNT],
172    receiver: tokio::sync::mpsc::UnboundedReceiver<P>,
173    hooks: Vec<Box<dyn Hook>>,
174}
175
176impl<P> From<tokio::sync::mpsc::UnboundedReceiver<P>> for EventStream<P> {
177    fn from(receiver: tokio::sync::mpsc::UnboundedReceiver<P>) -> Self {
178        Self {
179            filter: [true; EventKind::COUNT],
180            receiver,
181            hooks: Vec::new(),
182        }
183    }
184}
185
186impl<P> EventStream<P> {
187    pub fn add_hook(&mut self, hook: Box<dyn Hook>) {
188        self.hooks.push(hook);
189    }
190
191    #[cfg(feature = "xftp")]
192    pub fn hook_xftp<C: 'static + Clone + ClientApi>(&mut self, client: C) -> xftp::XftpClient<C> {
193        let xftp_client = xftp::XftpClient::from(client);
194        let hook = xftp_client.clone();
195        self.add_hook(Box::new(hook));
196
197        xftp_client
198    }
199
200    pub fn set_filter<I: IntoIterator<Item = EventKind>>(&mut self, f: Filter<I>) -> &mut Self {
201        match f {
202            Filter::Accept(kinds) => {
203                self.reject_all();
204                for kind in kinds {
205                    self.filter[kind.as_usize()] = true;
206                }
207            }
208            Filter::AcceptAllExcept(kinds) => {
209                self.accept_all();
210                for kind in kinds {
211                    self.filter[kind.as_usize()] = false;
212                }
213            }
214            Filter::AcceptAll => self.accept_all(),
215        }
216
217        self
218    }
219
220    pub fn accept(&mut self, kind: EventKind) {
221        self.filter[kind.as_usize()] = true;
222    }
223
224    pub fn reject(&mut self, kind: EventKind) {
225        self.filter[kind.as_usize()] = false;
226    }
227
228    pub fn accept_all(&mut self) {
229        self.set_all(true);
230    }
231
232    pub fn reject_all(&mut self) {
233        self.set_all(false)
234    }
235
236    fn set_all(&mut self, new: bool) {
237        for old in &mut self.filter {
238            *old = new;
239        }
240    }
241}
242
243impl<P: EventParser> EventStream<P> {
244    /// Turns stream into a [`DispatchChain`] builder with the provided `ctx`. The `ctx` is an
245    /// arbitrary type that can be used within event handlers. Use [`dispatcher::Dispatcher::seq`] to add
246    /// sequential handlers: `AsyncFnMut(Arc<Ev>, &mut Ctx)`; or [`dispatcher::Dispatcher::on`] for concurrent
247    /// ones: `AsyncFn(Arc<Ev>, Ctx) where Ctx: 'static + Clone + Send`.
248    pub fn into_dispatcher<C>(self, ctx: C) -> DispatchChain<P, C> {
249        DispatchChain::with_ctx(self, ctx)
250    }
251
252    /// Waits for a particular event `Ev` **dropping** other events in the process. This method is
253    /// mostly useful in bot initialisation scenarios when the bot doesn't have any active users.
254    /// Misusing this method may result in not receiving user messages and other important events.
255    pub async fn wait_for<Ev: events::EventData>(&mut self) -> Result<Option<Arc<Ev>>, P::Error> {
256        self.reject_all();
257        self.accept(Ev::KIND);
258        let result = self.try_next().await;
259        self.accept_all();
260
261        let ev = result?;
262        Ok(ev.map(|ev| Ev::from_event(ev).unwrap()))
263    }
264
265    /// Waits for one one of the events in the `kinds` list **dropping** other events in the
266    /// process. Returns the first encountered event of the specified kind. This method is mostly
267    /// useful in bot initialisation scenarios when the bot doesn't have any active users. Misusing
268    /// this method may result in not receiving user messages and other important events.
269    pub async fn wait_for_any(
270        &mut self,
271        kinds: impl IntoIterator<Item = EventKind>,
272    ) -> Result<Option<Event>, P::Error> {
273        self.set_filter(Filter::Accept(kinds));
274        let result = self.try_next().await;
275        self.accept_all();
276        result
277    }
278
279    pub async fn stream_events<E, F>(mut self, mut f: F) -> Result<Self, E>
280    where
281        F: AsyncFnMut(Event) -> Result<StreamEvents, E>,
282        E: From<P::Error>,
283    {
284        while let Some(event) = self.try_next().await? {
285            if let StreamEvents::Break = f(event).await? {
286                break;
287            }
288        }
289
290        Ok(self)
291    }
292
293    pub async fn stream_events_with_ctx_mut<E, Ctx, F>(
294        mut self,
295        mut f: F,
296        mut ctx: Ctx,
297    ) -> Result<(Self, Ctx), E>
298    where
299        F: AsyncFnMut(Event, &mut Ctx) -> Result<StreamEvents, E>,
300        E: From<P::Error>,
301    {
302        while let Some(event) = self.try_next().await? {
303            if let StreamEvents::Break = f(event, &mut ctx).await? {
304                break;
305            }
306        }
307
308        Ok((self, ctx))
309    }
310
311    pub async fn stream_events_with_ctx_cloned<E, Ctx, F>(
312        mut self,
313        f: F,
314        ctx: Ctx,
315    ) -> Result<(Self, Ctx), E>
316    where
317        Ctx: Clone,
318        F: AsyncFn(Event, Ctx) -> Result<StreamEvents, E>,
319        E: From<P::Error>,
320    {
321        while let Some(event) = self.try_next().await? {
322            if let StreamEvents::Break = f(event, ctx.clone()).await? {
323                break;
324            }
325        }
326
327        Ok((self, ctx))
328    }
329}
330
331pub enum Filter<I: IntoIterator<Item = EventKind>> {
332    Accept(I),
333    AcceptAll,
334    AcceptAllExcept(I),
335}
336
337#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
338pub enum StreamEvents {
339    Break,
340    Continue,
341}
342
343impl<P: EventParser> Stream for EventStream<P> {
344    type Item = Result<Event, P::Error>;
345
346    fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
347        loop {
348            match self.receiver.poll_recv(cx) {
349                Poll::Ready(Some(raw_event)) => {
350                    let kind = match raw_event.parse_kind() {
351                        Ok(kind) => kind,
352                        Err(e) => break Poll::Ready(Some(Err(e))),
353                    };
354
355                    if !self.hooks.iter().any(|h| h.should_intercept(kind))
356                        && !self.filter[kind.as_usize()]
357                    {
358                        continue;
359                    }
360
361                    match raw_event.parse_event() {
362                        Ok(event) => {
363                            for hook in self.hooks.iter_mut() {
364                                if hook.should_intercept(kind) {
365                                    hook.intercept_event(event.clone());
366                                }
367                            }
368
369                            if self.filter[kind.as_usize()] {
370                                break Poll::Ready(Some(Ok(event)));
371                            }
372                        }
373                        Err(e) => break Poll::Ready(Some(Err(e))),
374                    }
375                }
376                Poll::Ready(None) => break Poll::Ready(None),
377                Poll::Pending => break Poll::Pending,
378            }
379        }
380    }
381}
382
383/// A helper trait meant to be implemented by raw event types
384pub trait EventParser {
385    type Error;
386
387    /// Should parse kind cheaply without allocations
388    fn parse_kind(&self) -> Result<EventKind, Self::Error>;
389
390    /// Parse the whole events
391    fn parse_event(&self) -> Result<Event, Self::Error>;
392}
393
394impl EventParser for Event {
395    type Error = std::convert::Infallible;
396
397    fn parse_kind(&self) -> Result<EventKind, Self::Error> {
398        Ok(self.kind())
399    }
400
401    fn parse_event(&self) -> Result<Event, Self::Error> {
402        // Cheap Arc Clone
403        Ok(self.clone())
404    }
405}
406
407pub trait Hook {
408    fn should_intercept(&self, kind: EventKind) -> bool;
409
410    /// Hooks must not block the event stream; this method should be a cheap synchronous call.
411    /// Delegate heavy work to another thread or spawn async tasks internally.
412    fn intercept_event(&mut self, event: Event);
413}
414
415/// Syntactic sugar for constructing [`Preferences`](simploxide_api_types::Preferences) values.
416///
417/// ```ignore
418/// Preferences {
419///     timed_messages: preferences::timed_messages::yes(Duration::from_hours(4)),
420///     full_delete: preferences::YES,
421///     reactions: preferences::ALWAYS,
422///     voice: preferences::NO,
423///     files: preferences::ALWAYS,
424///     calls: preferences::YES,
425///     sessions: preferences::NO,
426///     commands: None,
427///     undocumented: Default::default(),
428/// }
429/// ```
430pub mod preferences {
431    use simploxide_api_types::{FeatureAllowed, SimplePreference};
432
433    pub mod timed_messages {
434        use super::*;
435        use simploxide_api_types::TimedMessagesPreference;
436
437        pub const TTL_MAX: std::time::Duration = std::time::Duration::from_hours(8784);
438
439        pub fn ttl_to_secs(ttl: std::time::Duration) -> i32 {
440            let clamped = std::cmp::min(ttl, TTL_MAX);
441            clamped.as_secs() as i32
442        }
443
444        pub fn always(ttl: std::time::Duration) -> Option<TimedMessagesPreference> {
445            Some(TimedMessagesPreference {
446                allow: FeatureAllowed::Always,
447                ttl: Some(ttl_to_secs(ttl)),
448                undocumented: serde_json::Value::Null,
449            })
450        }
451
452        pub fn yes(ttl: std::time::Duration) -> Option<TimedMessagesPreference> {
453            Some(TimedMessagesPreference {
454                allow: FeatureAllowed::Yes,
455                ttl: Some(ttl_to_secs(ttl)),
456                undocumented: serde_json::Value::Null,
457            })
458        }
459
460        pub const NO: Option<TimedMessagesPreference> = Some(TimedMessagesPreference {
461            allow: FeatureAllowed::No,
462            ttl: None,
463            undocumented: serde_json::Value::Null,
464        });
465    }
466
467    pub const ALWAYS: Option<SimplePreference> = Some(SimplePreference {
468        allow: FeatureAllowed::Always,
469        undocumented: serde_json::Value::Null,
470    });
471
472    pub const YES: Option<SimplePreference> = Some(SimplePreference {
473        allow: FeatureAllowed::Yes,
474        undocumented: serde_json::Value::Null,
475    });
476
477    pub const NO: Option<SimplePreference> = Some(SimplePreference {
478        allow: FeatureAllowed::No,
479        undocumented: serde_json::Value::Null,
480    });
481}