1pub 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#[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
81impl Client {
84 pub fn version(&self) -> impl Future<Output = Result<SimplexVersion, VersionError>> {
85 self.inner.version()
86 }
87
88 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
149pub 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 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 pub fn with_default_user(mut self, user: DefaultUser) -> Self {
181 self.default_user = Some(user);
182 self
183 }
184
185 pub fn auto_accept(mut self) -> Self {
187 self.auto_accept = Some(String::default());
188 self
189 }
190
191 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 pub fn with_avatar(mut self, avatar: ImagePreview) -> Self {
199 self.avatar = Some(avatar);
200 self
201 }
202
203 pub fn with_profile(mut self, profile: Profile) -> Self {
205 self.profile = Some(profile);
206 self
207 }
208
209 pub fn with_preferences(mut self, prefs: Preferences) -> Self {
211 self.preferences = Some(prefs);
212 self
213 }
214
215 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 pub fn max_instances(mut self, instances: usize) -> Self {
223 self.worker_config.max_instances = Some(instances);
224 self
225 }
226
227 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#[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 Ffi(CoreInitError),
322 VersionError(VersionError),
324 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#[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}