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