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