1pub 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#[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
80impl Client {
83 pub fn version(&self) -> impl Future<Output = Result<SimplexVersion, VersionError>> {
84 self.inner.version()
85 }
86
87 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
148pub 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 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 pub fn with_default_user(mut self, user: DefaultUser) -> Self {
180 self.default_user = Some(user);
181 self
182 }
183
184 pub fn auto_accept(mut self) -> Self {
186 self.auto_accept = Some(String::default());
187 self
188 }
189
190 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 pub fn with_avatar(mut self, avatar: ImagePreview) -> Self {
198 self.avatar = Some(avatar);
199 self
200 }
201
202 pub fn with_profile(mut self, profile: Profile) -> Self {
204 self.profile = Some(profile);
205 self
206 }
207
208 pub fn with_preferences(mut self, prefs: Preferences) -> Self {
210 self.preferences = Some(prefs);
211 self
212 }
213
214 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 pub fn max_instances(mut self, instances: usize) -> Self {
222 self.worker_config.max_instances = Some(instances);
223 self
224 }
225
226 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#[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 Ffi(CoreInitError),
321 VersionError(VersionError),
323 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#[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}