1use std::sync::Arc;
8
9pub use simploxide_ws_core::{
10 self as core, Error as CoreError, Event as CoreEvent, Result as CoreResult, SimplexVersion,
11 VersionError, tungstenite::Error as WsError,
12};
13
14#[cfg(feature = "cli")]
15pub use simploxide_ws_core::cli;
16
17use serde::Deserialize;
18use simploxide_api_types::{
19 Preferences, Profile,
20 client_api::{ExtractResponse, WebSocketResponseShape, WebSocketResponseShapeInner},
21 events::{Event, EventKind},
22};
23use simploxide_core::{MAX_SUPPORTED_VERSION, MIN_SUPPORTED_VERSION};
24use simploxide_ws_core::RawClient;
25
26use crate::{
27 BadResponseError, ClientApi, ClientApiError, EventParser,
28 bot::{BotName, BotProfileSettings, BotSettings},
29 id::UserId,
30 preview::ImagePreview,
31 util,
32};
33
34pub type EventResult = CoreResult<CoreEvent>;
35pub type EventStream = crate::EventStream<EventResult>;
36pub type ClientResult<T = ()> = ::std::result::Result<T, ClientError>;
37
38#[cfg(not(feature = "xftp"))]
39pub type Bot = crate::bot::Bot<Client>;
40
41#[cfg(feature = "xftp")]
42pub type Bot = crate::bot::Bot<crate::xftp::XftpClient<Client>>;
43
44#[cfg(feature = "farm")]
45pub type FarmBot = crate::bot::farm::FarmBot<Client>;
46
47#[cfg(feature = "farm")]
48pub type InitFarm = crate::bot::farm::InitFarm<Client, EventResult>;
49
50#[cfg(feature = "farm")]
51pub type RunningFarm = crate::bot::farm::RunningFarm<Client, EventResult>;
52
53pub async fn connect<S: AsRef<str>>(uri: S) -> Result<(Client, EventStream), ConnectError> {
67 let (raw_client, raw_event_queue) = simploxide_ws_core::connect(uri.as_ref()).await?;
68
69 let version = raw_client
70 .version()
71 .await
72 .map_err(ConnectError::VersionError)?;
73
74 if !version.is_supported() {
75 return Err(ConnectError::VersionMismatch(version));
76 }
77
78 Ok((
79 Client::from(raw_client),
80 EventStream::from(raw_event_queue.into_receiver()),
81 ))
82}
83
84pub async fn retry_connect<S: AsRef<str>>(
99 uri: S,
100 retry_delay: std::time::Duration,
101 mut retries_count: usize,
102) -> Result<(Client, EventStream), ConnectError> {
103 loop {
104 match connect(uri.as_ref()).await {
105 Ok(connection) => break Ok(connection),
106 Err(e) if !e.is_server() || retries_count == 0 => break Err(e),
107 Err(_) => {
108 retries_count -= 1;
109 tokio::time::sleep(retry_delay).await
110 }
111 }
112 }
113}
114
115impl EventParser for EventResult {
116 type Error = ClientError;
117
118 fn parse_kind(&self) -> Result<EventKind, Self::Error> {
119 match parse_data::<util::TypeField<'_>>(self) {
120 Ok(f) => Ok(EventKind::from_type_str(f.typ)),
121 Err(ClientError::BadResponse(BadResponseError::Undocumented(_))) => {
122 Ok(EventKind::Undocumented)
123 }
124 Err(e) => Err(e),
125 }
126 }
127
128 fn parse_user_id(&self) -> Result<Option<UserId>, Self::Error> {
129 match parse_data::<util::UserField>(self) {
130 Ok(f) => Ok(UserId::try_from(f.user.user_id).ok()),
131 Err(ClientError::BadResponse(_)) => Ok(None),
132 Err(e) => Err(e),
133 }
134 }
135
136 fn parse_event(&self) -> Result<Event, Self::Error> {
137 match parse_data(self) {
138 Ok(ev) => Ok(ev),
139 Err(ClientError::BadResponse(BadResponseError::Undocumented(json))) => {
140 Ok(Event::Undocumented(json))
141 }
142 Err(e) => Err(e),
143 }
144 }
145}
146
147fn parse_data<'de, 'r: 'de, D: 'de + Deserialize<'de>>(res: &'r EventResult) -> ClientResult<D> {
148 res.as_ref()
149 .map_err(|e| ClientError::WebSocketFailure(e.clone()))
150 .and_then(|ev| {
151 serde_json::from_str::<EventShape<D>>(ev)
152 .map_err(BadResponseError::InvalidJson)
153 .and_then(|shape| shape.extract_response())
154 .map_err(ClientError::BadResponse)
155 })
156}
157
158#[derive(Deserialize)]
159#[serde(untagged)]
160pub enum EventShape<T> {
161 ResponseShape(WebSocketResponseShape<T>),
162 InlineShape(WebSocketResponseShapeInner<T>),
163}
164
165impl<'de, T: 'de + Deserialize<'de>> ExtractResponse<'de, T> for EventShape<T> {
166 fn extract_response(self) -> Result<T, BadResponseError> {
167 match self {
168 Self::ResponseShape(resp) => resp.extract_response(),
169 Self::InlineShape(inline) => inline.extract_response(),
170 }
171 }
172}
173
174#[derive(Clone)]
177pub struct Client {
178 inner: RawClient,
179}
180
181impl From<RawClient> for Client {
182 fn from(inner: RawClient) -> Self {
183 Self { inner }
184 }
185}
186
187impl Client {
188 pub fn version(&self) -> impl Future<Output = Result<SimplexVersion, VersionError>> {
189 self.inner.version()
190 }
191
192 pub fn disconnect(self) -> impl Future<Output = ()> {
195 self.inner.disconnect()
196 }
197}
198
199impl ClientApi for Client {
200 type ResponseShape<'de, T>
201 = WebSocketResponseShape<T>
202 where
203 T: 'de + Deserialize<'de>;
204
205 type Error = ClientError;
206
207 async fn send_raw(&self, command: String) -> Result<String, Self::Error> {
208 self.inner
209 .send(command)
210 .await
211 .map_err(ClientError::WebSocketFailure)
212 }
213}
214
215#[derive(Debug)]
222pub enum ClientError {
223 WebSocketFailure(CoreError),
226 BadResponse(BadResponseError),
228}
229
230impl std::error::Error for ClientError {
231 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
232 match self {
233 Self::WebSocketFailure(error) => Some(error),
234 Self::BadResponse(error) => Some(error),
235 }
236 }
237}
238
239impl std::fmt::Display for ClientError {
240 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
241 match self {
242 ClientError::WebSocketFailure(err) => writeln!(f, "Web socket failure: {err}"),
243 ClientError::BadResponse(err) => err.fmt(f),
244 }
245 }
246}
247
248impl From<BadResponseError> for ClientError {
249 fn from(err: BadResponseError) -> Self {
250 Self::BadResponse(err)
251 }
252}
253
254impl ClientApiError for ClientError {
255 fn bad_response(&self) -> Option<&BadResponseError> {
256 if let Self::BadResponse(resp) = self {
257 Some(resp)
258 } else {
259 None
260 }
261 }
262
263 fn bad_response_mut(&mut self) -> Option<&mut BadResponseError> {
264 if let Self::BadResponse(resp) = self {
265 Some(resp)
266 } else {
267 None
268 }
269 }
270}
271
272#[derive(Debug)]
273pub enum ConnectError {
274 Server(CoreError),
276 VersionError(VersionError),
278 VersionMismatch(SimplexVersion),
280}
281
282impl ConnectError {
283 pub fn is_server(&self) -> bool {
284 matches!(self, Self::Server(_))
285 }
286
287 pub fn is_version_mismatch(&self) -> bool {
288 matches!(self, Self::VersionMismatch(_))
289 }
290}
291
292impl From<WsError> for ConnectError {
293 fn from(value: WsError) -> Self {
294 Self::Server(Arc::new(value))
295 }
296}
297
298impl std::fmt::Display for ConnectError {
299 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
300 match self {
301 Self::Server(error) => write!(f, "Cannot connect to the server: {error}"),
302 Self::VersionError(error) => write!(f, "Cannot get the server version: {error}"),
303 Self::VersionMismatch(v) => write!(
304 f,
305 "Version {v} is unsupported by the current client. Supported versions are {MIN_SUPPORTED_VERSION}..{MAX_SUPPORTED_VERSION}"
306 ),
307 }
308 }
309}
310
311impl std::error::Error for ConnectError {
312 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
313 match self {
314 Self::Server(error) => Some(error),
315 Self::VersionError(error) => Some(error),
316 Self::VersionMismatch(_) => None,
317 }
318 }
319}
320
321#[derive(Clone)]
322pub struct BotBuilder {
323 name: BotName,
324 port: u16,
325 retry_delay: std::time::Duration,
326 retries: usize,
327 auto_accept: Option<String>,
328 profile: Option<Profile>,
329 preferences: Option<Preferences>,
330 avatar: Option<ImagePreview>,
331 bio: Option<String>,
332 description: Option<String>,
333 #[cfg(feature = "cli")]
334 db_prefix: String,
335 #[cfg(feature = "cli")]
336 db_key: Option<String>,
337 #[cfg(feature = "cli")]
338 extra_args: Vec<std::ffi::OsString>,
339}
340
341impl BotBuilder {
342 pub fn new(name: impl Into<BotName>, port: u16) -> Self {
343 Self {
344 name: name.into(),
345 port,
346 retry_delay: std::time::Duration::from_secs(1),
347 retries: 5,
348 auto_accept: None,
349 profile: None,
350 preferences: None,
351 avatar: None,
352 bio: None,
353 description: None,
354 #[cfg(feature = "cli")]
355 db_prefix: "bot".into(),
356 #[cfg(feature = "cli")]
357 db_key: None,
358 #[cfg(feature = "cli")]
359 extra_args: Vec::new(),
360 }
361 }
362
363 #[cfg(feature = "cli")]
364 pub fn db_prefix(mut self, prefix: impl Into<String>) -> Self {
369 self.db_prefix = prefix.into();
370 self
371 }
372
373 #[cfg(feature = "cli")]
374 pub fn db_key(mut self, key: impl Into<String>) -> Self {
376 self.db_key = Some(key.into());
377 self
378 }
379
380 pub fn connect_retry_delay(mut self, delay: std::time::Duration) -> Self {
382 self.retry_delay = delay;
383 self
384 }
385
386 pub fn retries(mut self, n: usize) -> Self {
388 self.retries = n;
389 self
390 }
391
392 pub fn auto_accept(mut self) -> Self {
394 self.auto_accept = Some(String::default());
395 self
396 }
397
398 pub fn auto_accept_with(mut self, welcome_message: impl Into<String>) -> Self {
400 self.auto_accept = Some(welcome_message.into());
401 self
402 }
403
404 pub fn with_avatar(mut self, avatar: ImagePreview) -> Self {
406 self.avatar = Some(avatar);
407 self
408 }
409
410 pub fn with_bio(mut self, bio: impl Into<String>) -> Self {
412 self.bio = Some(bio.into());
413 self
414 }
415
416 pub fn with_description(mut self, description: impl Into<String>) -> Self {
418 self.description = Some(description.into());
419 self
420 }
421
422 pub fn with_profile(mut self, profile: Profile) -> Self {
424 self.profile = Some(profile);
425 self
426 }
427
428 pub fn with_preferences(mut self, prefs: Preferences) -> Self {
430 self.preferences = Some(prefs);
431 self
432 }
433
434 #[cfg(feature = "cli")]
436 pub fn cli_args<I, S>(mut self, args: I) -> Self
437 where
438 I: IntoIterator<Item = S>,
439 S: Into<std::ffi::OsString>,
440 {
441 self.extra_args.extend(args.into_iter().map(|s| s.into()));
442 self
443 }
444
445 pub async fn connect(self) -> Result<(Bot, EventStream), BotInitError> {
447 let url = format!("ws://127.0.0.1:{}", self.port);
448
449 let (client, events) = retry_connect(url, self.retry_delay, self.retries)
450 .await
451 .map_err(BotInitError::Connect)?;
452
453 #[cfg(feature = "xftp")]
454 let (client, events) = events.hook_xftp(client);
455
456 let settings = BotSettings {
457 display_name: self.name,
458 auto_accept: self.auto_accept,
459 profile_settings: match (self.profile, self.preferences) {
460 (Some(mut profile), Some(preferences)) => {
461 profile.preferences = Some(preferences);
462 Some(BotProfileSettings::FullProfile(profile))
463 }
464 (Some(profile), None) => Some(BotProfileSettings::FullProfile(profile)),
465 (None, Some(preferences)) => Some(BotProfileSettings::Preferences(preferences)),
466 (None, None) => None,
467 },
468 avatar: self.avatar,
469 bio: self.bio,
470 description: self.description,
471 };
472
473 let bot = Bot::init(client, settings).await?;
474
475 let mut events = events;
476 events.set_owner(bot.user_id());
477
478 Ok((bot, events))
479 }
480
481 #[cfg(feature = "cli")]
486 pub async fn launch(mut self) -> Result<(Bot, EventStream, cli::SimplexCli), BotInitError> {
487 let mut builder = cli::SimplexCli::builder(self.name.current(), self.port)
488 .db_prefix(std::mem::take(&mut self.db_prefix));
489
490 if let Some(ref mut key) = self.db_key {
491 builder = builder.db_key(std::mem::take(key));
492 }
493
494 let cli = builder
495 .args(std::mem::take(&mut self.extra_args))
496 .spawn()
497 .await
498 .map_err(BotInitError::CliSpawn)?;
499
500 let (bot, events) = self.connect().await?;
501 Ok((bot, events, cli))
502 }
503}
504
505#[cfg(feature = "farm")]
506#[derive(Clone)]
507pub struct BotFarmBuilder {
508 name: String,
509 port: u16,
510 retry_delay: std::time::Duration,
511 retries: usize,
512 #[cfg(feature = "cli")]
513 db_prefix: String,
514 #[cfg(feature = "cli")]
515 db_key: Option<String>,
516 #[cfg(feature = "cli")]
517 extra_args: Vec<std::ffi::OsString>,
518}
519
520#[cfg(feature = "farm")]
521impl BotFarmBuilder {
522 pub fn new(name: impl Into<String>, port: u16) -> Self {
523 Self {
524 name: name.into(),
525 port,
526 retry_delay: std::time::Duration::from_secs(1),
527 retries: 5,
528 #[cfg(feature = "cli")]
529 db_prefix: "bot".into(),
530 #[cfg(feature = "cli")]
531 db_key: None,
532 #[cfg(feature = "cli")]
533 extra_args: Vec::new(),
534 }
535 }
536
537 #[cfg(feature = "cli")]
538 pub fn db_prefix(mut self, prefix: impl Into<String>) -> Self {
543 self.db_prefix = prefix.into();
544 self
545 }
546
547 #[cfg(feature = "cli")]
548 pub fn db_key(mut self, key: impl Into<String>) -> Self {
550 self.db_key = Some(key.into());
551 self
552 }
553
554 pub fn connect_retry_delay(mut self, delay: std::time::Duration) -> Self {
556 self.retry_delay = delay;
557 self
558 }
559
560 pub fn retries(mut self, n: usize) -> Self {
562 self.retries = n;
563 self
564 }
565
566 #[cfg(feature = "cli")]
567 pub fn cli_args<I, S>(mut self, args: I) -> Self
569 where
570 I: IntoIterator<Item = S>,
571 S: Into<std::ffi::OsString>,
572 {
573 self.extra_args.extend(args.into_iter().map(|s| s.into()));
574 self
575 }
576
577 pub async fn connect(self) -> Result<InitFarm, BotInitError> {
579 let url = format!("ws://127.0.0.1:{}", self.port);
580
581 let (client, events) = retry_connect(url, self.retry_delay, self.retries)
582 .await
583 .map_err(BotInitError::Connect)?;
584
585 let farm = crate::bot::BotFarm::init(self.name, client, events).await?;
586 Ok(farm)
587 }
588
589 #[cfg(feature = "cli")]
590 pub async fn launch(mut self) -> Result<(InitFarm, cli::SimplexCli), BotInitError> {
595 let mut builder = cli::SimplexCli::builder(&self.name, self.port)
596 .db_prefix(std::mem::take(&mut self.db_prefix));
597
598 if let Some(ref mut key) = self.db_key {
599 builder = builder.db_key(std::mem::take(key));
600 }
601
602 let cli = builder
603 .args(std::mem::take(&mut self.extra_args))
604 .spawn()
605 .await
606 .map_err(BotInitError::CliSpawn)?;
607
608 let farm = self.connect().await?;
609 Ok((farm, cli))
610 }
611}
612
613#[derive(Debug)]
615pub enum BotInitError {
616 Connect(ConnectError),
617 Api(ClientError),
618 #[cfg(feature = "cli")]
619 CliSpawn(std::io::Error),
620}
621
622impl std::fmt::Display for BotInitError {
623 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
624 match self {
625 #[cfg(feature = "cli")]
626 Self::CliSpawn(e) => write!(f, "failed to spawn simplex-chat: {e}"),
627 Self::Connect(e) => write!(f, "websocket connection failed: {e}"),
628 Self::Api(e) => write!(f, "SimpleX API error during init: {e}"),
629 }
630 }
631}
632
633impl std::error::Error for BotInitError {
634 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
635 match self {
636 #[cfg(feature = "cli")]
637 Self::CliSpawn(e) => Some(e),
638 Self::Connect(e) => Some(e),
639 Self::Api(e) => Some(e),
640 }
641 }
642}
643
644impl From<ClientError> for BotInitError {
645 fn from(e: ClientError) -> Self {
646 Self::Api(e)
647 }
648}