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