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