pub use simploxide_ffi_core::{
CallError, DbOpts, DefaultUser, InitError as CoreInitError, SimplexVersion, WorkerConfig,
};
use simploxide_api_types::{
Preferences, Profile,
client_api::{ExtractResponse as _, FfiResponseShape},
events::{Event, EventKind},
};
use simploxide_core::{MAX_SUPPORTED_VERSION, MIN_SUPPORTED_VERSION};
use simploxide_ffi_core::{Event as CoreEvent, RawClient, Result as CoreResult, VersionError};
use std::sync::Arc;
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 init(
default_user: DefaultUser,
db_opts: DbOpts,
) -> Result<(Client, EventStream), InitError> {
init_with_config(default_user, db_opts, WorkerConfig::default()).await
}
pub async fn init_with_config(
default_user: DefaultUser,
db_opts: DbOpts,
config: WorkerConfig,
) -> Result<(Client, EventStream), InitError> {
let (raw_client, raw_event_queue) =
simploxide_ffi_core::init_with_config(default_user, db_opts, config).await?;
let version = raw_client
.version()
.await
.map_err(InitError::VersionError)?;
if !version.is_supported() {
return Err(InitError::VersionMismatch(version));
}
Ok((
Client::from(raw_client),
EventStream::from(raw_event_queue.into_receiver()),
))
}
#[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>
= FfiResponseShape<T>
where
T: 'de + serde::Deserialize<'de>;
type Error = ClientError;
async fn send_raw(&self, command: String) -> Result<String, Self::Error> {
self.inner
.send(command)
.await
.map_err(ClientError::FfiFailure)
}
}
impl EventParser for CoreResult<CoreEvent> {
type Error = ClientError;
fn parse_kind(&self) -> Result<EventKind, Self::Error> {
#[derive(serde::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 + serde::Deserialize<'de>>(
result: &'r CoreResult<CoreEvent>,
) -> Result<D, ClientError> {
result
.as_ref()
.map_err(|e| ClientError::FfiFailure(e.clone()))
.and_then(|ev| {
serde_json::from_str::<FfiResponseShape<D>>(ev)
.map_err(BadResponseError::InvalidJson)
.and_then(|shape| shape.extract_response())
.map_err(ClientError::BadResponse)
})
}
pub struct BotBuilder {
display_name: String,
db_opts: DbOpts,
default_user: Option<DefaultUser>,
auto_accept: Option<String>,
profile: Option<Profile>,
preferences: Option<Preferences>,
avatar: Option<ImagePreview>,
worker_config: WorkerConfig,
}
impl BotBuilder {
pub fn new(name: impl Into<String>, db_opts: DbOpts) -> Self {
Self {
display_name: name.into(),
db_opts,
default_user: None,
auto_accept: None,
profile: None,
preferences: None,
avatar: None,
worker_config: WorkerConfig::default(),
}
}
pub fn with_default_user(mut self, user: DefaultUser) -> Self {
self.default_user = Some(user);
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
}
pub fn max_event_latency(mut self, latency: std::time::Duration) -> Self {
self.worker_config.max_event_latency = Some(latency);
self
}
pub fn max_instances(mut self, instances: usize) -> Self {
self.worker_config.max_instances = Some(instances);
self
}
pub async fn launch(
self,
) -> Result<(Bot, crate::EventStream<CoreResult<CoreEvent>>), BotInitError> {
let default_user = self
.default_user
.unwrap_or_else(|| DefaultUser::bot(&self.display_name));
let (client, events) = init_with_config(default_user, self.db_opts, self.worker_config)
.await
.map_err(BotInitError::Init)?;
#[cfg(feature = "xftp")]
let (client, events) = {
let mut events = events;
let client = events.hook_xftp(client);
(client, events)
};
let settings = BotSettings {
display_name: self.display_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))
}
}
#[derive(Debug)]
pub enum ClientError {
FfiFailure(Arc<CallError>),
BadResponse(BadResponseError),
}
impl std::error::Error for ClientError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::FfiFailure(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::FfiFailure(err) => writeln!(f, "FFI error: {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 InitError {
Ffi(CoreInitError),
VersionError(VersionError),
VersionMismatch(SimplexVersion),
}
impl InitError {
pub fn is_ffi(&self) -> bool {
matches!(self, Self::Ffi(_))
}
pub fn is_version_mismatch(&self) -> bool {
matches!(self, Self::VersionMismatch(_))
}
}
impl From<CoreInitError> for InitError {
fn from(value: CoreInitError) -> Self {
Self::Ffi(value)
}
}
impl std::fmt::Display for InitError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Ffi(error) => write!(f, "Cannot initialize the FFI backend: {error}"),
Self::VersionError(error) => write!(f, "Cannot get FFI 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 InitError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Ffi(error) => Some(error),
Self::VersionError(error) => Some(error),
Self::VersionMismatch(_) => None,
}
}
}
#[derive(Debug)]
pub enum BotInitError {
Init(InitError),
Api(ClientError),
}
impl std::fmt::Display for BotInitError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Init(e) => write!(f, "SimpleX FFI init 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 {
Self::Init(e) => Some(e),
Self::Api(e) => Some(e),
}
}
}
impl From<ClientError> for BotInitError {
fn from(e: ClientError) -> Self {
Self::Api(e)
}
}