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