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
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: String,
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 #[cfg(feature = "cli")]
332 db_prefix: String,
333 #[cfg(feature = "cli")]
334 db_key: Option<String>,
335 #[cfg(feature = "cli")]
336 extra_args: Vec<std::ffi::OsString>,
337}
338
339impl BotBuilder {
340 pub fn new(name: impl Into<String>, port: u16) -> Self {
341 Self {
342 name: name.into(),
343 port,
344 retry_delay: std::time::Duration::from_secs(1),
345 retries: 5,
346 auto_accept: None,
347 profile: None,
348 preferences: None,
349 avatar: None,
350 #[cfg(feature = "cli")]
351 db_prefix: "bot".into(),
352 #[cfg(feature = "cli")]
353 db_key: None,
354 #[cfg(feature = "cli")]
355 extra_args: Vec::new(),
356 }
357 }
358
359 #[cfg(feature = "cli")]
360 pub fn db_prefix(mut self, prefix: impl Into<String>) -> Self {
365 self.db_prefix = prefix.into();
366 self
367 }
368
369 #[cfg(feature = "cli")]
370 pub fn db_key(mut self, key: impl Into<String>) -> Self {
372 self.db_key = Some(key.into());
373 self
374 }
375
376 pub fn connect_retry_delay(mut self, delay: std::time::Duration) -> Self {
378 self.retry_delay = delay;
379 self
380 }
381
382 pub fn retries(mut self, n: usize) -> Self {
384 self.retries = n;
385 self
386 }
387
388 pub fn auto_accept(mut self) -> Self {
390 self.auto_accept = Some(String::default());
391 self
392 }
393
394 pub fn auto_accept_with(mut self, welcome_message: impl Into<String>) -> Self {
396 self.auto_accept = Some(welcome_message.into());
397 self
398 }
399
400 pub fn with_avatar(mut self, avatar: ImagePreview) -> Self {
402 self.avatar = Some(avatar);
403 self
404 }
405
406 pub fn with_profile(mut self, profile: Profile) -> Self {
408 self.profile = Some(profile);
409 self
410 }
411
412 pub fn with_preferences(mut self, prefs: Preferences) -> Self {
414 self.preferences = Some(prefs);
415 self
416 }
417
418 #[cfg(feature = "cli")]
420 pub fn cli_args<I, S>(mut self, args: I) -> Self
421 where
422 I: IntoIterator<Item = S>,
423 S: Into<std::ffi::OsString>,
424 {
425 self.extra_args.extend(args.into_iter().map(|s| s.into()));
426 self
427 }
428
429 pub async fn connect(self) -> Result<(Bot, EventStream), BotInitError> {
431 let url = format!("ws://127.0.0.1:{}", self.port);
432
433 let (client, events) = retry_connect(url, self.retry_delay, self.retries)
434 .await
435 .map_err(BotInitError::Connect)?;
436
437 #[cfg(feature = "xftp")]
438 let (client, events) = events.hook_xftp(client);
439
440 let settings = BotSettings {
441 display_name: self.name,
442 auto_accept: self.auto_accept,
443 profile_settings: match (self.profile, self.preferences) {
444 (Some(mut profile), Some(preferences)) => {
445 profile.preferences = Some(preferences);
446 Some(BotProfileSettings::FullProfile(profile))
447 }
448 (Some(profile), None) => Some(BotProfileSettings::FullProfile(profile)),
449 (None, Some(preferences)) => Some(BotProfileSettings::Preferences(preferences)),
450 (None, None) => None,
451 },
452 avatar: self.avatar,
453 };
454
455 let bot = Bot::init(client, settings).await?;
456
457 let mut events = events;
458 events.set_owner(bot.user_id());
459
460 Ok((bot, events))
461 }
462
463 #[cfg(feature = "cli")]
468 pub async fn launch(mut self) -> Result<(Bot, EventStream, cli::SimplexCli), BotInitError> {
469 let mut builder = cli::SimplexCli::builder(&self.name, self.port)
470 .db_prefix(std::mem::take(&mut self.db_prefix));
471
472 if let Some(ref mut key) = self.db_key {
473 builder = builder.db_key(std::mem::take(key));
474 }
475
476 let cli = builder
477 .args(std::mem::take(&mut self.extra_args))
478 .spawn()
479 .await
480 .map_err(BotInitError::CliSpawn)?;
481
482 let (bot, events) = self.connect().await?;
483 Ok((bot, events, cli))
484 }
485}
486
487#[cfg(feature = "farm")]
488#[derive(Clone)]
489pub struct BotFarmBuilder {
490 name: String,
491 port: u16,
492 retry_delay: std::time::Duration,
493 retries: usize,
494 #[cfg(feature = "cli")]
495 db_prefix: String,
496 #[cfg(feature = "cli")]
497 db_key: Option<String>,
498 #[cfg(feature = "cli")]
499 extra_args: Vec<std::ffi::OsString>,
500}
501
502#[cfg(feature = "farm")]
503impl BotFarmBuilder {
504 pub fn new(name: impl Into<String>, port: u16) -> Self {
505 Self {
506 name: name.into(),
507 port,
508 retry_delay: std::time::Duration::from_secs(1),
509 retries: 5,
510 #[cfg(feature = "cli")]
511 db_prefix: "bot".into(),
512 #[cfg(feature = "cli")]
513 db_key: None,
514 #[cfg(feature = "cli")]
515 extra_args: Vec::new(),
516 }
517 }
518
519 #[cfg(feature = "cli")]
520 pub fn db_prefix(mut self, prefix: impl Into<String>) -> Self {
525 self.db_prefix = prefix.into();
526 self
527 }
528
529 #[cfg(feature = "cli")]
530 pub fn db_key(mut self, key: impl Into<String>) -> Self {
532 self.db_key = Some(key.into());
533 self
534 }
535
536 pub fn connect_retry_delay(mut self, delay: std::time::Duration) -> Self {
538 self.retry_delay = delay;
539 self
540 }
541
542 pub fn retries(mut self, n: usize) -> Self {
544 self.retries = n;
545 self
546 }
547
548 #[cfg(feature = "cli")]
549 pub fn cli_args<I, S>(mut self, args: I) -> Self
551 where
552 I: IntoIterator<Item = S>,
553 S: Into<std::ffi::OsString>,
554 {
555 self.extra_args.extend(args.into_iter().map(|s| s.into()));
556 self
557 }
558
559 pub async fn connect(self) -> Result<InitFarm, BotInitError> {
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(mut self) -> Result<(InitFarm, cli::SimplexCli), BotInitError> {
577 let mut builder = cli::SimplexCli::builder(&self.name, self.port)
578 .db_prefix(std::mem::take(&mut self.db_prefix));
579
580 if let Some(ref mut key) = self.db_key {
581 builder = builder.db_key(std::mem::take(key));
582 }
583
584 let cli = builder
585 .args(std::mem::take(&mut self.extra_args))
586 .spawn()
587 .await
588 .map_err(BotInitError::CliSpawn)?;
589
590 let farm = self.connect().await?;
591 Ok((farm, cli))
592 }
593}
594
595#[derive(Debug)]
597pub enum BotInitError {
598 Connect(ConnectError),
599 Api(ClientError),
600 #[cfg(feature = "cli")]
601 CliSpawn(std::io::Error),
602}
603
604impl std::fmt::Display for BotInitError {
605 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
606 match self {
607 #[cfg(feature = "cli")]
608 Self::CliSpawn(e) => write!(f, "failed to spawn simplex-chat: {e}"),
609 Self::Connect(e) => write!(f, "websocket connection failed: {e}"),
610 Self::Api(e) => write!(f, "SimpleX API error during init: {e}"),
611 }
612 }
613}
614
615impl std::error::Error for BotInitError {
616 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
617 match self {
618 #[cfg(feature = "cli")]
619 Self::CliSpawn(e) => Some(e),
620 Self::Connect(e) => Some(e),
621 Self::Api(e) => Some(e),
622 }
623 }
624}
625
626impl From<ClientError> for BotInitError {
627 fn from(e: ClientError) -> Self {
628 Self::Api(e)
629 }
630}