use std::sync::Arc;
pub use simploxide_ws_core::{
self as core, Error as CoreError, Event as CoreEvent, Result as CoreResult, SimplexVersion,
VersionError, tungstenite::Error as WsError,
};
#[cfg(feature = "cli")]
pub use simploxide_ws_core::cli;
use serde::Deserialize;
use simploxide_api_types::{
Preferences, Profile,
client_api::{ExtractResponse, WebSocketResponseShape, WebSocketResponseShapeInner},
events::{Event, EventKind},
};
use simploxide_core::{MAX_SUPPORTED_VERSION, MIN_SUPPORTED_VERSION};
use simploxide_ws_core::RawClient;
use crate::{
BadResponseError, ClientApi, ClientApiError, EventParser,
bot::{BotProfileSettings, BotSettings},
preview::ImagePreview,
};
#[cfg(not(feature = "xftp"))]
pub type Bot = crate::bot::Bot<Client>;
#[cfg(feature = "xftp")]
pub type Bot = crate::bot::Bot<crate::xftp::XftpClient<Client>>;
pub type EventStream = crate::EventStream<CoreResult<CoreEvent>>;
pub type ClientResult<T = ()> = ::std::result::Result<T, ClientError>;
pub async fn connect<S: AsRef<str>>(uri: S) -> Result<(Client, EventStream), ConnectError> {
let (raw_client, raw_event_queue) = simploxide_ws_core::connect(uri.as_ref()).await?;
let version = raw_client
.version()
.await
.map_err(ConnectError::VersionError)?;
if !version.is_supported() {
return Err(ConnectError::VersionMismatch(version));
}
Ok((
Client::from(raw_client),
EventStream::from(raw_event_queue.into_receiver()),
))
}
pub async fn retry_connect<S: AsRef<str>>(
uri: S,
retry_delay: std::time::Duration,
mut retries_count: usize,
) -> Result<(Client, EventStream), ConnectError> {
loop {
match connect(uri.as_ref()).await {
Ok(connection) => break Ok(connection),
Err(e) if !e.is_server() || retries_count == 0 => break Err(e),
Err(_) => {
retries_count -= 1;
tokio::time::sleep(retry_delay).await
}
}
}
}
impl EventParser for CoreResult<String> {
type Error = ClientError;
fn parse_kind(&self) -> Result<EventKind, Self::Error> {
#[derive(Deserialize)]
struct TypeField<'a> {
#[serde(rename = "type", borrow)]
typ: &'a str,
}
match parse_data::<TypeField<'_>>(self) {
Ok(f) => Ok(EventKind::from_type_str(f.typ)),
Err(ClientError::BadResponse(BadResponseError::Undocumented(_))) => {
Ok(EventKind::Undocumented)
}
Err(e) => Err(e),
}
}
fn parse_event(&self) -> Result<Event, Self::Error> {
parse_data(self)
}
}
fn parse_data<'de, 'r: 'de, D: 'de + Deserialize<'de>>(
res: &'r CoreResult<String>,
) -> ClientResult<D> {
res.as_ref()
.map_err(|e| ClientError::WebSocketFailure(e.clone()))
.and_then(|ev| {
serde_json::from_str::<EventShape<D>>(ev)
.map_err(BadResponseError::InvalidJson)
.and_then(|shape| shape.extract_response())
.map_err(ClientError::BadResponse)
})
}
#[derive(Deserialize)]
#[serde(untagged)]
pub enum EventShape<T> {
ResponseShape(WebSocketResponseShape<T>),
InlineShape(WebSocketResponseShapeInner<T>),
}
impl<'de, T: 'de + Deserialize<'de>> ExtractResponse<'de, T> for EventShape<T> {
fn extract_response(self) -> Result<T, BadResponseError> {
match self {
Self::ResponseShape(resp) => resp.extract_response(),
Self::InlineShape(inline) => inline.extract_response(),
}
}
}
#[derive(Clone)]
pub struct Client {
inner: RawClient,
}
impl From<RawClient> for Client {
fn from(inner: RawClient) -> Self {
Self { inner }
}
}
impl Client {
pub fn version(&self) -> impl Future<Output = Result<SimplexVersion, VersionError>> {
self.inner.version()
}
pub fn disconnect(self) -> impl Future<Output = ()> {
self.inner.disconnect()
}
}
impl ClientApi for Client {
type ResponseShape<'de, T>
= WebSocketResponseShape<T>
where
T: 'de + Deserialize<'de>;
type Error = ClientError;
async fn send_raw(&self, command: String) -> Result<String, Self::Error> {
self.inner
.send(command)
.await
.map_err(ClientError::WebSocketFailure)
}
}
#[derive(Debug)]
pub enum ClientError {
WebSocketFailure(CoreError),
BadResponse(BadResponseError),
}
impl std::error::Error for ClientError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::WebSocketFailure(error) => Some(error),
Self::BadResponse(error) => Some(error),
}
}
}
impl std::fmt::Display for ClientError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ClientError::WebSocketFailure(err) => writeln!(f, "Web socket failure: {err}"),
ClientError::BadResponse(err) => err.fmt(f),
}
}
}
impl From<BadResponseError> for ClientError {
fn from(err: BadResponseError) -> Self {
Self::BadResponse(err)
}
}
impl ClientApiError for ClientError {
fn bad_response(&self) -> Option<&BadResponseError> {
if let Self::BadResponse(resp) = self {
Some(resp)
} else {
None
}
}
fn bad_response_mut(&mut self) -> Option<&mut BadResponseError> {
if let Self::BadResponse(resp) = self {
Some(resp)
} else {
None
}
}
}
#[derive(Debug)]
pub enum ConnectError {
Server(CoreError),
VersionError(VersionError),
VersionMismatch(SimplexVersion),
}
impl ConnectError {
pub fn is_server(&self) -> bool {
matches!(self, Self::Server(_))
}
pub fn is_version_mismatch(&self) -> bool {
matches!(self, Self::VersionMismatch(_))
}
}
impl From<WsError> for ConnectError {
fn from(value: WsError) -> Self {
Self::Server(Arc::new(value))
}
}
impl std::fmt::Display for ConnectError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Server(error) => write!(f, "Cannot connect to the server: {error}"),
Self::VersionError(error) => write!(f, "Cannot get the server version: {error}"),
Self::VersionMismatch(v) => write!(
f,
"Version {v} is unsupported by the current client. Supported versions are {MIN_SUPPORTED_VERSION}..{MAX_SUPPORTED_VERSION}"
),
}
}
}
impl std::error::Error for ConnectError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Server(error) => Some(error),
Self::VersionError(error) => Some(error),
Self::VersionMismatch(_) => None,
}
}
}
pub struct BotBuilder {
name: String,
port: u16,
retry_delay: std::time::Duration,
retries: usize,
auto_accept: Option<String>,
profile: Option<Profile>,
preferences: Option<Preferences>,
avatar: Option<ImagePreview>,
#[cfg(feature = "cli")]
db_prefix: String,
#[cfg(feature = "cli")]
db_key: Option<String>,
#[cfg(feature = "cli")]
extra_args: Vec<std::ffi::OsString>,
}
impl BotBuilder {
pub fn new(name: impl Into<String>, port: u16) -> Self {
Self {
name: name.into(),
port,
db_prefix: "bot".into(),
db_key: None,
retry_delay: std::time::Duration::from_secs(1),
retries: 5,
auto_accept: None,
profile: None,
preferences: None,
avatar: None,
#[cfg(feature = "cli")]
extra_args: Vec::new(),
}
}
#[cfg(feature = "cli")]
pub fn db_prefix(mut self, prefix: impl Into<String>) -> Self {
self.db_prefix = prefix.into();
self
}
#[cfg(feature = "cli")]
pub fn db_key(mut self, key: impl Into<String>) -> Self {
self.db_key = Some(key.into());
self
}
pub fn connect_retry_delay(mut self, delay: std::time::Duration) -> Self {
self.retry_delay = delay;
self
}
pub fn retries(mut self, n: usize) -> Self {
self.retries = n;
self
}
pub fn auto_accept(mut self) -> Self {
self.auto_accept = Some(String::default());
self
}
pub fn auto_accept_with(mut self, welcome_message: impl Into<String>) -> Self {
self.auto_accept = Some(welcome_message.into());
self
}
pub fn with_avatar(mut self, avatar: ImagePreview) -> Self {
self.avatar = Some(avatar);
self
}
pub fn with_profile(mut self, profile: Profile) -> Self {
self.profile = Some(profile);
self
}
pub fn with_preferences(mut self, prefs: Preferences) -> Self {
self.preferences = Some(prefs);
self
}
#[cfg(feature = "cli")]
pub fn cli_args<I, S>(mut self, args: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<std::ffi::OsString>,
{
self.extra_args.extend(args.into_iter().map(|s| s.into()));
self
}
pub async fn connect(self) -> Result<(Bot, EventStream), BotInitError> {
let url = format!("ws://127.0.0.1:{}", self.port);
let (client, events) = retry_connect(url, self.retry_delay, self.retries)
.await
.map_err(BotInitError::Connect)?;
#[cfg(feature = "xftp")]
let (client, events) = {
let mut events = events;
let client = events.hook_xftp(client);
(client, events)
};
let settings = BotSettings {
display_name: self.name,
auto_accept: self.auto_accept,
profile_settings: match (self.profile, self.preferences) {
(Some(mut profile), Some(preferences)) => {
profile.preferences = Some(preferences);
Some(BotProfileSettings::FullProfile(profile))
}
(Some(profile), None) => Some(BotProfileSettings::FullProfile(profile)),
(None, Some(preferences)) => Some(BotProfileSettings::Preferences(preferences)),
(None, None) => None,
},
avatar: self.avatar,
};
let bot = Bot::init(client, settings).await?;
Ok((bot, events))
}
#[cfg(feature = "cli")]
pub async fn launch(mut self) -> Result<(Bot, EventStream, cli::SimplexCli), BotInitError> {
let mut builder = cli::SimplexCli::builder(&self.name, self.port)
.db_prefix(std::mem::take(&mut self.db_prefix));
if let Some(ref mut key) = self.db_key {
builder = builder.db_key(std::mem::take(key));
}
let cli = builder
.args(std::mem::take(&mut self.extra_args))
.spawn()
.await
.map_err(BotInitError::CliSpawn)?;
let (bot, events) = self.connect().await?;
Ok((bot, events, cli))
}
}
#[derive(Debug)]
pub enum BotInitError {
Connect(ConnectError),
Api(ClientError),
#[cfg(feature = "cli")]
CliSpawn(std::io::Error),
}
impl std::fmt::Display for BotInitError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
#[cfg(feature = "cli")]
Self::CliSpawn(e) => write!(f, "failed to spawn simplex-chat: {e}"),
Self::Connect(e) => write!(f, "websocket connection failed: {e}"),
Self::Api(e) => write!(f, "SimpleX API error during init: {e}"),
}
}
}
impl std::error::Error for BotInitError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
#[cfg(feature = "cli")]
Self::CliSpawn(e) => Some(e),
Self::Connect(e) => Some(e),
Self::Api(e) => Some(e),
}
}
}
impl From<ClientError> for BotInitError {
fn from(e: ClientError) -> Self {
Self::Api(e)
}
}