Skip to main content

simploxide_client/
ffi.rs

1//! FFI backend that embeds the SimpleX-Chat library in-process via native Rust bindings.
2//!
3//! Use [`BotBuilder`] to initialise the FFI runtime and get a ready-to-use [`Bot`].
4//! For lower-level access, [`init`] and [`init_with_config`] return a [`Client`] and an
5//! [`EventStream`](crate::EventStream) directly.
6//!
7//! Requires AGPL-3.0 and additional build configuration. See `simploxide-sxcrt-sys`.
8
9pub use simploxide_ffi_core::{
10    CallError, DbOpts, DefaultUser, InitError as CoreInitError, SimplexVersion, VersionError,
11    WorkerConfig,
12};
13
14use simploxide_api_types::{
15    Preferences, Profile,
16    client_api::{ExtractResponse as _, FfiResponseShape},
17    events::{Event, EventKind},
18};
19use simploxide_core::{MAX_SUPPORTED_VERSION, MIN_SUPPORTED_VERSION};
20use simploxide_ffi_core::{Event as CoreEvent, RawClient, Result as CoreResult};
21
22use std::sync::Arc;
23
24use crate::{
25    BadResponseError, ClientApi, ClientApiError, EventParser,
26    bot::{BotProfileSettings, BotSettings},
27    id::UserId,
28    preview::ImagePreview,
29    util,
30};
31
32#[cfg(not(feature = "xftp"))]
33pub type Bot = crate::bot::Bot<Client>;
34
35#[cfg(feature = "xftp")]
36pub type Bot = crate::bot::Bot<crate::xftp::XftpClient<Client>>;
37
38#[cfg(feature = "farm")]
39pub type FarmBot = crate::bot::farm::FarmBot<Client>;
40
41pub type EventStream = crate::EventStream<CoreResult<CoreEvent>>;
42pub type ClientResult<T = ()> = ::std::result::Result<T, ClientError>;
43
44pub async fn init(
45    default_user: DefaultUser,
46    db_opts: DbOpts,
47) -> Result<(Client, EventStream), InitError> {
48    init_with_config(default_user, db_opts, WorkerConfig::default()).await
49}
50
51pub async fn init_with_config(
52    default_user: DefaultUser,
53    db_opts: DbOpts,
54    config: WorkerConfig,
55) -> Result<(Client, EventStream), InitError> {
56    let (raw_client, raw_event_queue) =
57        simploxide_ffi_core::init_with_config(default_user, db_opts, config).await?;
58
59    let version = raw_client
60        .version()
61        .await
62        .map_err(InitError::VersionError)?;
63
64    if !version.is_supported() {
65        return Err(InitError::VersionMismatch(version));
66    }
67
68    Ok((
69        Client::from(raw_client),
70        EventStream::from(raw_event_queue.into_receiver()),
71    ))
72}
73
74/// A cheaply clonable high-level FFI client implementing [`ClientApi`]
75#[derive(Clone)]
76pub struct Client {
77    inner: RawClient,
78}
79
80impl From<RawClient> for Client {
81    fn from(inner: RawClient) -> Self {
82        Self { inner }
83    }
84}
85
86/// A high level SimpleX-Chat client which provides typed API methods with automatic command
87/// serialization and response deserialization.
88impl Client {
89    pub fn version(&self) -> impl Future<Output = Result<SimplexVersion, VersionError>> {
90        self.inner.version()
91    }
92
93    /// Initiates a graceful shutdown for the underlying web socket connection. See
94    /// [`simploxide_ffi_core::RawClient::disconnect`] for details.
95    pub fn disconnect(self) -> impl Future<Output = ()> {
96        self.inner.disconnect()
97    }
98}
99
100impl ClientApi for Client {
101    type ResponseShape<'de, T>
102        = FfiResponseShape<T>
103    where
104        T: 'de + serde::Deserialize<'de>;
105
106    type Error = ClientError;
107
108    async fn send_raw(&self, command: String) -> Result<String, Self::Error> {
109        self.inner
110            .send(command)
111            .await
112            .map_err(ClientError::FfiFailure)
113    }
114}
115
116impl EventParser for CoreResult<CoreEvent> {
117    type Error = ClientError;
118
119    fn parse_kind(&self) -> Result<EventKind, Self::Error> {
120        match parse_data::<util::TypeField<'_>>(self) {
121            Ok(f) => Ok(EventKind::from_type_str(f.typ)),
122            // FFI chat error shapes are the same for events and responses which confuses the parser therefore
123            // chat errors must be handled manually
124            Err(ClientError::BadResponse(BadResponseError::ChatError(_))) => {
125                Ok(EventKind::ChatError)
126            }
127            Err(ClientError::BadResponse(BadResponseError::Undocumented(_))) => {
128                Ok(EventKind::Undocumented)
129            }
130            Err(e) => Err(e),
131        }
132    }
133
134    fn parse_user_id(&self) -> Result<Option<UserId>, Self::Error> {
135        match parse_data::<util::UserField>(self) {
136            Ok(f) => Ok(UserId::try_from(f.user.user_id).ok()),
137            Err(ClientError::BadResponse(_)) => Ok(None),
138            Err(e) => Err(e),
139        }
140    }
141
142    fn parse_event(&self) -> Result<Event, Self::Error> {
143        match parse_data(self) {
144            Ok(ev) => Ok(ev),
145            // FFI chat error shapes are the same for events and responses which confuses the parser therefore
146            // chat errors must be handled manually
147            Err(ClientError::BadResponse(BadResponseError::ChatError(err))) => Ok(
148                Event::ChatError(Arc::new(simploxide_api_types::events::ChatError {
149                    chat_error: err.as_ref().clone(),
150                    undocumented: Default::default(),
151                })),
152            ),
153            Err(ClientError::BadResponse(BadResponseError::Undocumented(json))) => {
154                Ok(Event::Undocumented(json))
155            }
156            Err(e) => Err(e),
157        }
158    }
159}
160
161fn parse_data<'de, 'r: 'de, D: 'de + serde::Deserialize<'de>>(
162    result: &'r CoreResult<CoreEvent>,
163) -> Result<D, ClientError> {
164    result
165        .as_ref()
166        .map_err(|e| ClientError::FfiFailure(e.clone()))
167        .and_then(|ev| {
168            serde_json::from_str::<FfiResponseShape<D>>(ev)
169                .map_err(BadResponseError::InvalidJson)
170                .and_then(|shape| shape.extract_response())
171                .map_err(ClientError::BadResponse)
172        })
173}
174
175/// Builder for an FFI-backed [`Bot`].
176#[derive(Clone)]
177pub struct BotBuilder {
178    display_name: String,
179    db_opts: DbOpts,
180    default_user: Option<DefaultUser>,
181    auto_accept: Option<String>,
182    profile: Option<Profile>,
183    preferences: Option<Preferences>,
184    avatar: Option<ImagePreview>,
185    worker_config: WorkerConfig,
186}
187
188impl BotBuilder {
189    /// Build a bot account (default).
190    pub fn new(name: impl Into<String>, db_opts: DbOpts) -> Self {
191        Self {
192            display_name: name.into(),
193            db_opts,
194            default_user: None,
195            auto_accept: None,
196            profile: None,
197            preferences: None,
198            avatar: None,
199            worker_config: WorkerConfig::default(),
200        }
201    }
202
203    /// Override the default user created for empty databases.
204    ///
205    /// By default the default user name matches the bot name. This setting allows to create a user
206    /// different from an active bot
207    pub fn with_default_user(mut self, user: DefaultUser) -> Self {
208        self.default_user = Some(user);
209        self
210    }
211
212    /// Create public address and auto accept users
213    pub fn auto_accept(mut self) -> Self {
214        self.auto_accept = Some(String::default());
215        self
216    }
217
218    /// [Self::auto_accept] with a welcome message
219    pub fn auto_accept_with(mut self, welcome_message: impl Into<String>) -> Self {
220        self.auto_accept = Some(welcome_message.into());
221        self
222    }
223
224    /// Set the bot avatar during initialisation
225    pub fn with_avatar(mut self, avatar: ImagePreview) -> Self {
226        self.avatar = Some(avatar);
227        self
228    }
229
230    /// Update/create the whole bot profile on launch
231    pub fn with_profile(mut self, profile: Profile) -> Self {
232        self.profile = Some(profile);
233        self
234    }
235
236    /// Apply these preferences to the bot's profile during initialisation.
237    pub fn with_preferences(mut self, prefs: Preferences) -> Self {
238        self.preferences = Some(prefs);
239        self
240    }
241
242    /// Set max permissible event latency. See [`WorkerConfig::max_event_latency`] for details
243    pub fn max_event_latency(mut self, latency: std::time::Duration) -> Self {
244        self.worker_config.max_event_latency = Some(latency);
245        self
246    }
247
248    /// Set max concurrent SimpleX-Chat instances. See [`WorkerConfig::max_instances`] for details
249    pub fn max_instances(mut self, instances: usize) -> Self {
250        self.worker_config.max_instances = Some(instances);
251        self
252    }
253
254    /// Initialise the SimpleX FFI runtime and return a ready-to-use bot.
255    pub async fn launch(
256        self,
257    ) -> Result<(Bot, crate::EventStream<CoreResult<CoreEvent>>), BotInitError> {
258        let default_user = self
259            .default_user
260            .unwrap_or_else(|| DefaultUser::bot(&self.display_name));
261
262        let (client, events) = init_with_config(default_user, self.db_opts, self.worker_config)
263            .await
264            .map_err(BotInitError::Init)?;
265
266        #[cfg(feature = "xftp")]
267        let (client, events) = events.hook_xftp(client);
268
269        let settings = BotSettings {
270            display_name: self.display_name,
271            auto_accept: self.auto_accept,
272            profile_settings: match (self.profile, self.preferences) {
273                (Some(mut profile), Some(preferences)) => {
274                    profile.preferences = Some(preferences);
275                    Some(BotProfileSettings::FullProfile(profile))
276                }
277                (Some(profile), None) => Some(BotProfileSettings::FullProfile(profile)),
278                (None, Some(preferences)) => Some(BotProfileSettings::Preferences(preferences)),
279                (None, None) => None,
280            },
281            avatar: self.avatar,
282        };
283
284        let bot = Bot::init(client, settings).await?;
285
286        let mut events = events;
287        events.set_owner(bot.user_id());
288
289        Ok((bot, events))
290    }
291}
292
293#[cfg(feature = "farm")]
294#[derive(Clone)]
295pub struct BotFarmBuilder {
296    display_name: String,
297    db_opts: DbOpts,
298    default_user: Option<DefaultUser>,
299    worker_config: WorkerConfig,
300}
301
302#[cfg(feature = "farm")]
303impl BotFarmBuilder {
304    pub fn new(name: impl Into<String>, db_opts: DbOpts) -> Self {
305        Self {
306            display_name: name.into(),
307            db_opts,
308            default_user: None,
309            worker_config: WorkerConfig::default(),
310        }
311    }
312
313    /// Override the default user created for empty databases.
314    ///
315    /// By default the default user name matches the bot name. This setting allows to create a user
316    /// different from an active bot
317    pub fn with_default_user(mut self, user: DefaultUser) -> Self {
318        self.default_user = Some(user);
319        self
320    }
321
322    /// Set max permissible event latency. See [`WorkerConfig::max_event_latency`] for details
323    pub fn max_event_latency(mut self, latency: std::time::Duration) -> Self {
324        self.worker_config.max_event_latency = Some(latency);
325        self
326    }
327
328    /// Set max concurrent SimpleX-Chat instances. See [`WorkerConfig::max_instances`] for details
329    pub fn max_instances(mut self, instances: usize) -> Self {
330        self.worker_config.max_instances = Some(instances);
331        self
332    }
333
334    /// Initialise the SimpleX FFI runtime and return a farm
335    pub async fn launch(
336        self,
337    ) -> Result<
338        crate::bot::BotFarm<crate::bot::farm::Init<Client, CoreResult<CoreEvent>>>,
339        BotInitError,
340    > {
341        let default_user = self
342            .default_user
343            .unwrap_or_else(|| DefaultUser::bot(&self.display_name));
344
345        let (client, events) = init_with_config(default_user, self.db_opts, self.worker_config)
346            .await
347            .map_err(BotInitError::Init)?;
348
349        let bot = crate::bot::BotFarm::init(self.display_name, client, events).await?;
350        Ok(bot)
351    }
352}
353
354/// See [`crate::client_api::AllowUndocumentedResponses`] if you don't want to trigger an error
355/// when you receive undocumeted responses(you usually receive undocumented responses when your
356/// simplex-chat version is not compatible with the current simploxide-client version. Keep an eye
357/// on the [Version compatability table](https://github.com/a1akris/simploxide?tab=readme-ov-file#version-compatability-table))
358#[derive(Debug)]
359pub enum ClientError {
360    FfiFailure(Arc<CallError>),
361    BadResponse(BadResponseError),
362}
363
364impl std::error::Error for ClientError {
365    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
366        match self {
367            Self::FfiFailure(error) => Some(error),
368            Self::BadResponse(error) => Some(error),
369        }
370    }
371}
372
373impl std::fmt::Display for ClientError {
374    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
375        match self {
376            ClientError::FfiFailure(err) => writeln!(f, "FFI error: {err}"),
377            ClientError::BadResponse(err) => err.fmt(f),
378        }
379    }
380}
381
382impl From<BadResponseError> for ClientError {
383    fn from(err: BadResponseError) -> Self {
384        Self::BadResponse(err)
385    }
386}
387
388impl ClientApiError for ClientError {
389    fn bad_response(&self) -> Option<&BadResponseError> {
390        if let Self::BadResponse(resp) = self {
391            Some(resp)
392        } else {
393            None
394        }
395    }
396
397    fn bad_response_mut(&mut self) -> Option<&mut BadResponseError> {
398        if let Self::BadResponse(resp) = self {
399            Some(resp)
400        } else {
401            None
402        }
403    }
404}
405
406#[derive(Debug)]
407pub enum InitError {
408    /// Failure to init the FFI instance
409    Ffi(CoreInitError),
410    /// Failure to get the backend version
411    VersionError(VersionError),
412    /// Unsupported backend version
413    VersionMismatch(SimplexVersion),
414}
415
416impl InitError {
417    pub fn is_ffi(&self) -> bool {
418        matches!(self, Self::Ffi(_))
419    }
420
421    pub fn is_version_mismatch(&self) -> bool {
422        matches!(self, Self::VersionMismatch(_))
423    }
424}
425
426impl From<CoreInitError> for InitError {
427    fn from(value: CoreInitError) -> Self {
428        Self::Ffi(value)
429    }
430}
431
432impl std::fmt::Display for InitError {
433    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
434        match self {
435            Self::Ffi(error) => write!(f, "Cannot initialize the FFI backend: {error}"),
436            Self::VersionError(error) => write!(f, "Cannot get FFI version {error}"),
437            Self::VersionMismatch(v) => write!(
438                f,
439                "Version {v} is unsupported by the current client. Supported versions are {MIN_SUPPORTED_VERSION}..{MAX_SUPPORTED_VERSION}"
440            ),
441        }
442    }
443}
444
445impl std::error::Error for InitError {
446    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
447        match self {
448            Self::Ffi(error) => Some(error),
449            Self::VersionError(error) => Some(error),
450            Self::VersionMismatch(_) => None,
451        }
452    }
453}
454
455/// Error returned by [`BotBuilder::launch`].
456#[derive(Debug)]
457pub enum BotInitError {
458    Init(InitError),
459    Api(ClientError),
460}
461
462impl std::fmt::Display for BotInitError {
463    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
464        match self {
465            Self::Init(e) => write!(f, "SimpleX FFI init failed: {e}"),
466            Self::Api(e) => write!(f, "SimpleX API error during init: {e}"),
467        }
468    }
469}
470
471impl std::error::Error for BotInitError {
472    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
473        match self {
474            Self::Init(e) => Some(e),
475            Self::Api(e) => Some(e),
476        }
477    }
478}
479
480impl From<ClientError> for BotInitError {
481    fn from(e: ClientError) -> Self {
482        Self::Api(e)
483    }
484}