Skip to main content

simploxide_client/
lib.rs

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