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//! `simploxide` strives to be a minimal library for simple bots while also coming with batteries
88//! included for all sorts of the advanced use cases. The balance is maintained through feature
89//! gates documented below:
90//!
91//! - **`cli`** *(default)*: WebSocket backend ([`ws`]) with a built-in runner that spawns and
92//!   manages a local `simplex-chat` process. Use [`ws::BotBuilder::launch`] to start everything
93//!   in one call.
94//!
95//! - **`websocket`**: WebSocket backend ([`ws`]) without the CLI runner. Use
96//!   [`ws::BotBuilder::connect`] to attach to an already-running `simplex-chat` server.
97//!
98//! - **`ffi`**: FFI backend ([`ffi`]) that embeds the SimpleX-Chat library in-process.
99//!   Requires AGPL-3.0 and additional build configuration; see `simploxide-sxcrt-sys`.
100//!
101//! - **`native_crypto`**: Native Rust implementation of client-side encryption(XSalsa20 + Poly1305). Enables
102//!   [`ImagePreview::from_crypto_file`](preview::ImagePreview::from_crypto_file) and [crypto::fs]
103//!   module allowing to encrypt decrypt files directly in the Rust code
104//!
105//! - **`multimedia`**: Image transcoding via the `image` crate. Enables
106//!   [`preview::transcoder::Transcoder`] and automatic thumbnail generation for [`messages::Image`].
107//!   [`preview::ImagePreview`] automatically tries to transcode its sources to JPEGs with this
108//!   feature on
109//!
110//! - **`xftp`**: Enables [`xftp::XftpClient`], which streamlines file downloads via
111//!   `download_file` method.
112//!
113//! - **`cancellation`**: Re-exports [`tokio_util::sync::CancellationToken`] and enables helper
114//!   methods for cooperative shutdown.
115//!
116//! - **`crypto`**: Enables `zeroize` and `rand` crates and exposes interfaces allowing end-users
117//!   to reimplement SimpleX crypto in a way compatible with `simploxide`. Pulled in automatically by
118//!   `native_crypto`. Useful on its own if you wish to use your own crypto implementation.
119//!
120//! - **`farm`**: Enables bot farms that manage multiple bots on the same SimpleX instance.
121//!
122//! - **`fullcli`**: Convenience bundle: `cli` + `native_crypto` + `multimedia` + `xftp` +
123//!   `cancellation` + `farm`.
124//!
125//! - **`fullffi`**: Convenience bundle: `ffi` + `native_crypto` + `multimedia` + `xftp` +
126//!   `cancellation` + `farm`.
127//!
128//! ### How to work with this documentation?
129//!
130//! The [bot] page should be your primary reference and the [events] page your secondary one.
131//! From these two pages you should be able to find everything in a structured manner.
132
133#[cfg(feature = "crypto")]
134pub mod crypto;
135#[cfg(feature = "ffi")]
136pub mod ffi;
137#[cfg(feature = "websocket")]
138pub mod ws;
139#[cfg(feature = "xftp")]
140pub mod xftp;
141
142pub mod bot;
143pub mod dispatcher;
144pub mod ext;
145pub mod id;
146pub mod messages;
147pub mod prelude;
148pub mod preview;
149pub mod remote;
150
151mod util;
152
153pub use simploxide_api_types::{
154    self as types,
155    client_api::{self, BadResponseError, ClientApi, ClientApiError},
156    commands, events,
157    events::{Event, EventKind},
158    responses,
159    utils::CommandSyntax,
160};
161
162#[cfg(feature = "cancellation")]
163pub use tokio_util::{self, sync::CancellationToken};
164
165pub use dispatcher::DispatchChain;
166
167use futures::{Stream, TryStreamExt as _};
168
169use std::{
170    pin::Pin,
171    sync::Arc,
172    task::{Context, Poll},
173};
174
175use crate::id::UserId;
176
177/// The high level event stream that embeds event filtering.
178///
179/// Parsing SimpleX events may be costly, they are quite large deeply nested structs with a lot of
180/// [`String`] and [`std::collections::BTreeMap`] types. This stream provides filtering APIs
181/// allowing to parse and propagate events the application handles and drop all other events early
182/// without allocating any extra memory.
183///
184/// By default filters are disabled and no events are dropped. Use [`Self::set_filter`] to only
185/// receive events you're interested in.
186///
187/// Use [`Self::into_dispatcher`] to handle events conveniently. Dispatchers are completely
188/// zerocost, manage filters internally, and provide a high-level easy to use API covering the
189/// absolute majority of use cases.
190pub struct EventStream<P> {
191    user_filter: Option<UserFilter>,
192    kind_filter: [bool; EventKind::COUNT],
193    receiver: tokio::sync::mpsc::UnboundedReceiver<P>,
194    hooks: Vec<Arc<dyn Hook>>,
195}
196
197impl<P> FromIterator<P> for EventStream<P> {
198    fn from_iter<I: IntoIterator<Item = P>>(iter: I) -> Self {
199        let (sender, receiver) = tokio::sync::mpsc::unbounded_channel();
200
201        for item in iter {
202            sender.send(item).unwrap();
203        }
204
205        Self::from(receiver)
206    }
207}
208
209impl<P> From<tokio::sync::mpsc::UnboundedReceiver<P>> for EventStream<P> {
210    fn from(receiver: tokio::sync::mpsc::UnboundedReceiver<P>) -> Self {
211        Self {
212            user_filter: None,
213            kind_filter: [true; EventKind::COUNT],
214            receiver,
215            hooks: Vec::new(),
216        }
217    }
218}
219
220impl<P> EventStream<P> {
221    pub fn into_receiver(self) -> tokio::sync::mpsc::UnboundedReceiver<P> {
222        self.receiver
223    }
224
225    /// Allows to unconditionally intercept events as specified by the [`Hook`] trait
226    pub fn add_hook(&mut self, hook: Arc<dyn Hook>) -> &mut Self {
227        self.hooks.push(hook);
228        self
229    }
230
231    #[cfg(feature = "xftp")]
232    pub fn hook_xftp<C: 'static + Clone + Send + ClientApi>(
233        mut self,
234        client: C,
235    ) -> (xftp::XftpClient<C>, Self) {
236        let xftp_client = xftp::XftpClient::from(client);
237        let hook = xftp_client.manager();
238
239        self.add_hook(hook);
240
241        (xftp_client, self)
242    }
243
244    /// Setting this hook enables support for remote control sessions
245    ///
246    /// See [`remote::CtrlHandle`]
247    pub fn hook_remote_control(mut self) -> (Arc<remote::CtrlHandle>, Self) {
248        let handle = Arc::new(remote::CtrlHandle::new());
249        self.add_hook(handle.clone());
250
251        (handle, self)
252    }
253
254    /// Set stream owner. Events with different UserIds will be filtered out
255    pub fn set_owner(&mut self, id: id::UserId) -> &mut Self {
256        self.user_filter = Some(UserFilter::Include(id));
257        self
258    }
259
260    /// Events for the specified user ID will be filtered out.
261    ///
262    /// Currently, only a single user ID can be excluded, calling this method multiple times
263    /// overwrites the excluded user ID.
264    pub fn exclude_user(&mut self, id: id::UserId) -> &mut Self {
265        self.user_filter = Some(UserFilter::Exclude(id));
266        self
267    }
268
269    /// Remove stream/owner or user exclusion
270    pub fn unset_user(&mut self) -> &mut Self {
271        self.user_filter = None;
272        self
273    }
274
275    pub fn set_filter<I: IntoIterator<Item = EventKind>>(&mut self, f: Filter<I>) -> &mut Self {
276        match f {
277            Filter::Accept(kinds) => {
278                self.reject_all();
279                for kind in kinds {
280                    self.kind_filter[kind.as_usize()] = true;
281                }
282            }
283            Filter::AcceptAllExcept(kinds) => {
284                self.accept_all();
285                for kind in kinds {
286                    self.kind_filter[kind.as_usize()] = false;
287                }
288            }
289            Filter::AcceptAll => self.accept_all(),
290        }
291
292        self
293    }
294
295    pub fn accept(&mut self, kind: EventKind) {
296        self.kind_filter[kind.as_usize()] = true;
297    }
298
299    pub fn reject(&mut self, kind: EventKind) {
300        self.kind_filter[kind.as_usize()] = false;
301    }
302
303    pub fn accept_all(&mut self) {
304        self.set_all(true);
305    }
306
307    pub fn reject_all(&mut self) {
308        self.set_all(false)
309    }
310
311    /// After this call stream stops receiving new events. You still need to consume all buffered events for graceful cleanup.
312    ///
313    /// Use [Self::discard] if you want to drop all events gracefully
314    pub fn close(&mut self) {
315        self.receiver.close();
316    }
317
318    /// Discards the stream and executes a proper cleanup
319    pub async fn discard(mut self) {
320        self.close();
321        self.reject_all();
322
323        while self.receiver.recv().await.is_some() {}
324    }
325
326    fn set_all(&mut self, new: bool) {
327        for old in &mut self.kind_filter {
328            *old = new;
329        }
330    }
331
332    fn matches_user_filter(&self, owner: Option<id::UserId>) -> bool {
333        match (self.user_filter, owner) {
334            (Some(UserFilter::Include(user)), Some(owner)) => user == owner,
335            (Some(UserFilter::Exclude(user)), Some(owner)) => user != owner,
336            _ => true,
337        }
338    }
339}
340
341impl<P: EventParser> EventStream<P> {
342    /// Turns stream into a [`DispatchChain`] builder with the provided `ctx`. The `ctx` is an
343    /// arbitrary type that can be used within event handlers. Use [`dispatcher::Dispatcher::seq`] to add
344    /// sequential handlers: `AsyncFnMut(Arc<Ev>, &mut Ctx)`; or [`dispatcher::Dispatcher::on`] for concurrent
345    /// ones: `AsyncFn(Arc<Ev>, Ctx) where Ctx: 'static + Clone + Send`.
346    pub fn into_dispatcher<C>(self, ctx: C) -> DispatchChain<P, C> {
347        DispatchChain::with_ctx(self, ctx)
348    }
349
350    /// Waits for a particular event `Ev` **dropping** other events in the process. This method is
351    /// mostly useful in bot initialisation scenarios when the bot doesn't have any active users.
352    /// Misusing this method may result in not receiving user messages and other important events.
353    pub async fn wait_for<Ev: events::EventData>(&mut self) -> Result<Option<Arc<Ev>>, P::Error> {
354        self.reject_all();
355        self.accept(Ev::KIND);
356        let result = self.try_next().await;
357        self.accept_all();
358
359        let ev = result?;
360        Ok(ev.map(|ev| Ev::from_event(ev).unwrap()))
361    }
362
363    /// Waits for one one of the events in the `kinds` list **dropping** other events in the
364    /// process. Returns the first encountered event of the specified kind. This method is mostly
365    /// useful in bot initialisation scenarios when the bot doesn't have any active users. Misusing
366    /// this method may result in not receiving user messages and other important events.
367    pub async fn wait_for_any(
368        &mut self,
369        kinds: impl IntoIterator<Item = EventKind>,
370    ) -> Result<Option<Event>, P::Error> {
371        self.set_filter(Filter::Accept(kinds));
372        let result = self.try_next().await;
373        self.accept_all();
374        result
375    }
376
377    pub async fn stream_events<E, F>(mut self, mut f: F) -> Result<Self, E>
378    where
379        F: AsyncFnMut(Event) -> Result<StreamEvents, E>,
380        E: From<P::Error>,
381    {
382        while let Some(event) = self.try_next().await? {
383            if let StreamEvents::Break = f(event).await? {
384                break;
385            }
386        }
387
388        Ok(self)
389    }
390
391    pub async fn stream_events_with_ctx_mut<E, Ctx, F>(
392        mut self,
393        mut f: F,
394        mut ctx: Ctx,
395    ) -> Result<(Self, Ctx), E>
396    where
397        F: AsyncFnMut(Event, &mut Ctx) -> Result<StreamEvents, E>,
398        E: From<P::Error>,
399    {
400        while let Some(event) = self.try_next().await? {
401            if let StreamEvents::Break = f(event, &mut ctx).await? {
402                break;
403            }
404        }
405
406        Ok((self, ctx))
407    }
408
409    pub async fn stream_events_with_ctx_cloned<E, Ctx, F>(
410        mut self,
411        f: F,
412        ctx: Ctx,
413    ) -> Result<(Self, Ctx), E>
414    where
415        Ctx: Clone,
416        F: AsyncFn(Event, Ctx) -> Result<StreamEvents, E>,
417        E: From<P::Error>,
418    {
419        while let Some(event) = self.try_next().await? {
420            if let StreamEvents::Break = f(event, ctx.clone()).await? {
421                break;
422            }
423        }
424
425        Ok((self, ctx))
426    }
427}
428
429pub enum Filter<I: IntoIterator<Item = EventKind>> {
430    Accept(I),
431    AcceptAll,
432    AcceptAllExcept(I),
433}
434
435#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
436pub enum StreamEvents {
437    Break,
438    Continue,
439}
440
441impl<P: EventParser> Stream for EventStream<P> {
442    type Item = Result<Event, P::Error>;
443
444    fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
445        loop {
446            match self.receiver.poll_recv(cx) {
447                Poll::Ready(Some(raw_event)) => {
448                    match raw_event.parse_user_id() {
449                        Ok(owner) => {
450                            if !self.matches_user_filter(owner) {
451                                continue;
452                            }
453                        }
454                        Err(e) => break Poll::Ready(Some(Err(e))),
455                    };
456
457                    let kind = match raw_event.parse_kind() {
458                        Ok(kind) => kind,
459                        Err(e) => break Poll::Ready(Some(Err(e))),
460                    };
461
462                    if !self.hooks.iter().any(|h| h.should_intercept(kind))
463                        && !self.kind_filter[kind.as_usize()]
464                    {
465                        continue;
466                    }
467
468                    match raw_event.parse_event() {
469                        Ok(event) => {
470                            for hook in self.hooks.iter_mut() {
471                                if hook.should_intercept(kind) {
472                                    hook.intercept_event(event.clone());
473                                }
474                            }
475
476                            if self.kind_filter[kind.as_usize()] {
477                                break Poll::Ready(Some(Ok(event)));
478                            }
479                        }
480                        Err(e) => break Poll::Ready(Some(Err(e))),
481                    }
482                }
483                Poll::Ready(None) => break Poll::Ready(None),
484                Poll::Pending => break Poll::Pending,
485            }
486        }
487    }
488}
489
490/// A helper trait meant to be implemented by raw event types
491pub trait EventParser {
492    type Error;
493
494    /// Parse kind cheaply without allocations
495    fn parse_kind(&self) -> Result<EventKind, Self::Error>;
496
497    /// Parse user ID cheaply without allocations
498    fn parse_user_id(&self) -> Result<Option<id::UserId>, Self::Error>;
499
500    /// Parse the whole events
501    fn parse_event(&self) -> Result<Event, Self::Error>;
502}
503
504impl EventParser for Event {
505    type Error = std::convert::Infallible;
506
507    fn parse_kind(&self) -> Result<EventKind, Self::Error> {
508        Ok(self.kind())
509    }
510
511    fn parse_user_id(&self) -> Result<Option<id::UserId>, Self::Error> {
512        // SAFETY: In fully parsed event the ID cannot be zero.
513        Ok(self
514            .user_id()
515            .map(|id| unsafe { UserId::from_raw_unchecked(id) }))
516    }
517
518    fn parse_event(&self) -> Result<Event, Self::Error> {
519        // Cheap Arc Clone
520        Ok(self.clone())
521    }
522}
523
524pub trait Hook: 'static + Send + Sync {
525    /// Return true if you want to intercept the given event kind. [`Self::intercept_event`] won't
526    /// be called kinds this method returned false
527    fn should_intercept(&self, kind: EventKind) -> bool;
528
529    /// Hooks must not block the event stream; this method should be a cheap synchronous call.
530    /// Delegate heavy work to another thread or spawn async tasks internally.
531    fn intercept_event(&self, event: Event);
532}
533
534#[derive(Clone, Copy, PartialEq, Eq, Hash)]
535enum UserFilter {
536    Include(id::UserId),
537    Exclude(id::UserId),
538}
539
540/// Syntactic sugar for constructing [`Preferences`](simploxide_api_types::Preferences) values.
541///
542/// ```ignore
543/// Preferences {
544///     timed_messages: preferences::timed_messages::yes(Duration::from_hours(4)),
545///     full_delete: preferences::YES,
546///     reactions: preferences::ALWAYS,
547///     voice: preferences::NO,
548///     files: preferences::ALWAYS,
549///     calls: preferences::YES,
550///     sessions: preferences::NO,
551///     commands: None,
552///     undocumented: Default::default(),
553/// }
554/// ```
555pub mod preferences {
556    use simploxide_api_types::{FeatureAllowed, SimplePreference};
557
558    pub const ALWAYS: Option<SimplePreference> = Some(SimplePreference {
559        allow: FeatureAllowed::Always,
560        undocumented: serde_json::Value::Null,
561    });
562
563    pub const YES: Option<SimplePreference> = Some(SimplePreference {
564        allow: FeatureAllowed::Yes,
565        undocumented: serde_json::Value::Null,
566    });
567
568    pub const NO: Option<SimplePreference> = Some(SimplePreference {
569        allow: FeatureAllowed::No,
570        undocumented: serde_json::Value::Null,
571    });
572
573    pub mod timed_messages {
574        use super::*;
575        use simploxide_api_types::TimedMessagesPreference;
576
577        pub const TTL_MAX: std::time::Duration = std::time::Duration::from_hours(8784);
578
579        pub fn ttl_to_secs(ttl: std::time::Duration) -> i32 {
580            let clamped = std::cmp::min(ttl, TTL_MAX);
581            clamped.as_secs() as i32
582        }
583
584        pub fn always(ttl: std::time::Duration) -> Option<TimedMessagesPreference> {
585            Some(TimedMessagesPreference {
586                allow: FeatureAllowed::Always,
587                ttl: Some(ttl_to_secs(ttl)),
588                undocumented: serde_json::Value::Null,
589            })
590        }
591
592        pub fn yes(ttl: std::time::Duration) -> Option<TimedMessagesPreference> {
593            Some(TimedMessagesPreference {
594                allow: FeatureAllowed::Yes,
595                ttl: Some(ttl_to_secs(ttl)),
596                undocumented: serde_json::Value::Null,
597            })
598        }
599
600        pub const NO: Option<TimedMessagesPreference> = Some(TimedMessagesPreference {
601            allow: FeatureAllowed::No,
602            ttl: None,
603            undocumented: serde_json::Value::Null,
604        });
605    }
606
607    pub mod group {
608        use simploxide_api_types::{GroupFeatureEnabled, GroupPreference};
609
610        pub const YES: Option<GroupPreference> = Some(GroupPreference {
611            enable: GroupFeatureEnabled::On,
612            undocumented: serde_json::Value::Null,
613        });
614
615        pub const NO: Option<GroupPreference> = Some(GroupPreference {
616            enable: GroupFeatureEnabled::Off,
617            undocumented: serde_json::Value::Null,
618        });
619
620        pub mod timed_messages {
621            use crate::preferences::timed_messages::ttl_to_secs;
622            use simploxide_api_types::{GroupFeatureEnabled, TimedMessagesGroupPreference};
623
624            pub fn yes(ttl: std::time::Duration) -> Option<TimedMessagesGroupPreference> {
625                Some(TimedMessagesGroupPreference {
626                    enable: GroupFeatureEnabled::On,
627                    ttl: Some(ttl_to_secs(ttl)),
628                    undocumented: serde_json::Value::Null,
629                })
630            }
631
632            pub const NO: Option<TimedMessagesGroupPreference> =
633                Some(TimedMessagesGroupPreference {
634                    enable: GroupFeatureEnabled::Off,
635                    ttl: None,
636                    undocumented: serde_json::Value::Null,
637                });
638        }
639
640        pub mod role {
641            use simploxide_api_types::{GroupFeatureEnabled, GroupMemberRole, RoleGroupPreference};
642
643            pub const fn yes(role: GroupMemberRole) -> Option<RoleGroupPreference> {
644                Some(RoleGroupPreference {
645                    enable: GroupFeatureEnabled::On,
646                    role: Some(role),
647                    undocumented: serde_json::Value::Null,
648                })
649            }
650
651            /// **WARN:** This const was not tested and may be invalid
652            pub const NO: Option<RoleGroupPreference> = Some(RoleGroupPreference {
653                enable: GroupFeatureEnabled::Off,
654                role: None,
655                undocumented: serde_json::Value::Null,
656            });
657        }
658
659        pub mod support {
660            use simploxide_api_types::{GroupFeatureEnabled, SupportGroupPreference};
661
662            pub const YES: Option<SupportGroupPreference> = Some(SupportGroupPreference {
663                enable: GroupFeatureEnabled::On,
664                undocumented: serde_json::Value::Null,
665            });
666
667            pub const NO: Option<SupportGroupPreference> = Some(SupportGroupPreference {
668                enable: GroupFeatureEnabled::Off,
669                undocumented: serde_json::Value::Null,
670            });
671        }
672    }
673}