use_prelude!();
pub mod builder;
use std::{env, mem::ManuallyDrop, sync::RwLock};
use crossbeam_utils::atomic::AtomicCell;
use ffi_sdk::BoxedDitto;
use uuid::Uuid;
use self::builder::DittoBuilder;
use crate::{
auth::{DittoAuthenticator, ValidityListener},
error::{DittoError, ErrorKind, LicenseError},
identity::SharedIdentity,
transport::{
peers_observer::PeersObserver,
presence_manager_v2::{PresenceManagerV2Context, V2Presence},
TransportConfig, TransportSync,
},
utils::prelude::*,
};
#[doc(inline)]
pub use ffi_sdk::CLogLevel as LogLevel;
pub struct Ditto {
fields: ManuallyDrop<Arc<DittoFields>>,
}
impl std::ops::Deref for Ditto {
type Target = DittoFields;
#[inline]
fn deref(&'_ self) -> &'_ DittoFields {
&*self.fields
}
}
pub struct DittoFields {
ditto: Arc<ffi_sdk::BoxedDitto>,
ditto_root: Arc<dyn DittoRoot>, identity: SharedIdentity,
auth: Option<DittoAuthenticator>,
#[allow(dead_code)]
validity_listener: Option<Arc<ValidityListener>>, store: Store,
activated: AtomicCell<bool>,
site_id: SiteId,
transports: Arc<RwLock<TransportSync>>,
presence_manager_v2: Arc<PresenceManagerV2Context>,
}
impl Ditto {
fn drop_fields(fields: Arc<DittoFields>) {
impl Drop for Ditto {
fn drop(&mut self) {
self.stop_sync();
unsafe {
ffi_sdk::ditto_shutdown(&self.ditto);
}
unsafe {
Ditto::drop_fields(ManuallyDrop::take(&mut self.fields));
}
}
}
::log::debug!("Dropping Ditto instance");
let mut fields = Arc::try_unwrap(fields)
.ok()
.expect("outstanding strong back reference to Ditto");
if let Some(_witness_of_unicity) = Arc::get_mut(&mut fields.ditto) {
} else {
let count = Arc::strong_count(&fields.ditto);
::log::debug!("Attempting to drop Ditto with {} live references", count);
}
}
}
impl Ditto {
pub fn start_sync(&self) -> Result<(), DittoError> {
if self.activated.load().not() && self.identity.requires_offline_only_license_token() {
return Err(ErrorKind::NotActivated.into());
}
match self.transports.write() {
Ok(mut transports) => {
transports.start_sync();
Ok(())
}
Err(e) => {
let rust_error = format!("{:?}", e); Err(DittoError::new(ErrorKind::Internal, rust_error))
}
}
}
pub fn stop_sync(&self) {
if let Ok(mut transports) = self.transports.write() {
transports.stop_sync()
}
}
pub fn set_transport_config(&self, config: TransportConfig) {
if let Ok(mut transports) = self.transports.write() {
transports.set_transport_config(config);
}
}
pub fn current_transport_config(&self) -> Result<TransportConfig, DittoError> {
match self.transports.read() {
Ok(t) => Ok(t.current_config().clone()),
Err(e) => {
let msg = format!("transport config cannot be read {:?}", e);
Err(DittoError::new(ErrorKind::Internal, msg))
}
}
}
#[cfg(test)]
pub fn effective_transport_config(&self) -> Result<TransportConfig, DittoError> {
match self.transports.read() {
Ok(t) => Ok(t.effective_config().clone()),
Err(e) => {
let msg = format!("transport config cannot be read {:?}", e);
Err(DittoError::new(ErrorKind::Internal, msg))
}
}
}
}
impl Ditto {
pub fn with_sdk_version<R>(ret: impl FnOnce(&'_ str) -> R) -> R {
ret(unsafe { ffi_sdk::ditto_get_sdk_version().to_str() })
}
}
impl Ditto {
pub fn set_logging_enabled(enabled: bool) {
unsafe { ffi_sdk::ditto_logger_enabled(enabled) }
}
pub fn get_logging_enabled() -> bool {
unsafe { ffi_sdk::ditto_logger_enabled_get() }
}
pub fn get_emoji_log_level_headings_enabled() -> bool {
unsafe { ffi_sdk::ditto_logger_emoji_headings_enabled_get() }
}
pub fn set_emoji_log_level_headings_enabled(enabled: bool) {
unsafe {
ffi_sdk::ditto_logger_emoji_headings_enabled(enabled);
}
}
pub fn get_minimum_log_level() -> LogLevel {
unsafe { ffi_sdk::ditto_logger_minimum_log_level_get() }
}
pub fn set_minimum_log_level(log_level: LogLevel) {
unsafe {
ffi_sdk::ditto_logger_minimum_log_level(log_level);
}
}
}
impl Ditto {
pub fn set_offline_only_license_token(&self, license_token: &str) -> Result<(), DittoError> {
if self.identity.requires_offline_only_license_token() {
use ::safer_ffi::prelude::{AsOut, ManuallyDropMut};
use ffi_sdk::LicenseVerificationResult;
let c_license: char_p::Box = char_p::new(license_token);
let mut err_msg = None;
let out_err_msg = err_msg.manually_drop_mut().as_out();
let res = unsafe { ffi_sdk::verify_license(c_license.as_ref(), Some(out_err_msg)) };
if res == LicenseVerificationResult::LicenseOk {
self.activated.store(true);
return Ok(());
}
self.activated.store(false);
let err_msg = err_msg.unwrap();
::log::error!("{}", err_msg);
match res {
LicenseVerificationResult::LicenseExpired => {
Err(DittoError::license(LicenseError::LicenseTokenExpired {
message: err_msg.as_ref().to_string(),
}))
}
LicenseVerificationResult::VerificationFailed => Err(DittoError::license(
LicenseError::LicenseTokenVerificationFailed {
message: err_msg.as_ref().to_string(),
},
)),
LicenseVerificationResult::UnsupportedFutureVersion => Err(DittoError::license(
LicenseError::LicenseTokenUnsupportedFutureVersion {
message: err_msg.as_ref().to_string(),
},
)),
_ => panic!("Unexpected license verification result {:?}", res),
}
} else {
Err(DittoError::new(ErrorKind::Internal, "Offline license tokens should only be used for Manual, SharedKey or OfflinePlayground identities"))
}
}
pub fn set_license_from_env(&self, var_name: &str) -> Result<(), DittoError> {
match env::var(var_name) {
Ok(token) => self.set_offline_only_license_token(&token),
Err(env::VarError::NotPresent) => {
let msg = format!("No license token found for env var {}", &var_name);
Err(DittoError::from_str(ErrorKind::Config, msg))
}
Err(e) => Err(DittoError::new(ErrorKind::Config, e)),
}
}
pub fn store(&self) -> &Store {
&self.store
}
pub fn site_id(&self) -> u64 {
self.site_id
}
pub fn set_device_name(&self, name: &str) {
let c_device_name: char_p::Box = char_p::new(name.to_owned());
unsafe {
ffi_sdk::ditto_set_device_name(&self.ditto, c_device_name.as_ref());
}
}
pub fn transport_diagnostics(&self) -> TransportDiagnostics {
todo!();
}
#[deprecated(note = "use observe_peers instead")]
pub fn observe_peers_v2<H>(&self, handler: H) -> PeersObserver
where
H: Fn(V2Presence) + Send + Sync + 'static,
{
self.observe_peers(handler)
}
pub fn observe_peers<H>(&self, handler: H) -> PeersObserver
where
H: Fn(V2Presence) + Send + Sync + 'static,
{
self.presence_manager_v2
.add_observer(self.ditto.retain(), handler)
}
pub fn root_dir(&self) -> &Path {
self.ditto_root.root_path()
}
pub fn data_dir(&self) -> &Path {
self.ditto_root.data_path()
}
pub fn authenticator(&self) -> Option<DittoAuthenticator> {
self.auth.clone()
}
pub fn is_activated(&self) -> bool {
self.activated.load()
}
}
impl Ditto {
pub fn builder() -> DittoBuilder {
DittoBuilder::new()
}
pub fn new(app_id: AppId) -> Ditto {
Ditto::builder()
.with_root(Arc::new(
PersistentRoot::from_current_exe().expect("Invalid Ditto Root"),
))
.with_identity(|ditto_root| identity::OfflinePlayground::new(ditto_root, app_id))
.expect("Invalid Ditto Identity")
.with_minimum_log_level(LogLevel::Info)
.build()
.expect("Failed to build Ditto Instance")
}
pub(crate) fn new_with_fields(fields: Arc<DittoFields>) -> Ditto {
Ditto {
fields: ManuallyDrop::new(fields),
}
}
}
impl Ditto {
pub fn run_garbage_collection(&self) {
unsafe {
ffi_sdk::ditto_run_garbage_collection(&self.ditto);
}
}
}
pub struct TransportDiagnostics;
pub type SiteId = u64;
#[derive(Clone, Debug)]
pub struct AppId(String);
impl AppId {
pub fn generate() -> Self {
let uuid = uuid::Uuid::new_v4();
AppId::from_uuid(uuid)
}
pub fn from_uuid(uuid: Uuid) -> Self {
let id_str = format!("{:x}", &uuid); AppId(id_str)
}
pub fn from_env(var: &str) -> Result<Self, DittoError> {
let id_str = env::var(var).map_err(|err| DittoError::new(ErrorKind::Config, err))?;
Ok(AppId(id_str))
}
pub fn as_str(&self) -> &str {
&self.0
}
pub fn to_c_string(&self) -> char_p::Box {
char_p::new(self.0.as_str())
}
pub fn default_auth_url(&self) -> String {
format!("https://{}.cloud.ditto.live", self.0)
}
pub fn default_sync_url(&self) -> String {
format!("wss://{}.cloud.ditto.live", self.0)
}
}
use std::{fmt, fmt::Display, str::FromStr};
impl Display for AppId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
impl FromStr for AppId {
type Err = DittoError;
fn from_str(s: &str) -> Result<AppId, DittoError> {
Ok(AppId(s.to_string()))
}
}
#[cfg(test)]
mod tests {
use super::*;
pub fn get_offline_ditto() -> Result<Ditto, DittoError> {
let ditto = Ditto::builder()
.with_temp_dir()
.with_identity(|ditto_root| {
identity::OfflinePlayground::new(ditto_root, AppId::generate())
})?
.with_minimum_log_level(CLogLevel::Info)
.build()?;
ditto.set_license_from_env("DITTO_LICENSE").unwrap();
Ok(ditto)
}
#[test]
fn test_default() {
let ditto = get_offline_ditto().unwrap();
let effective_config = ditto.effective_transport_config().unwrap();
assert!(effective_config.connect.websocket_urls.is_empty());
}
#[test]
fn test_start_sync() {
let ditto = get_offline_ditto().unwrap();
let mut config = TransportConfig::new();
config.peer_to_peer.bluetooth_le.enabled = true;
config.peer_to_peer.lan.enabled = true;
config
.connect
.websocket_urls
.insert(String::from("ws://ditto.com"));
ditto.set_transport_config(config);
ditto.start_sync().unwrap();
let effective_config = ditto.effective_transport_config().unwrap();
assert!(!effective_config.connect.websocket_urls.is_empty());
let tr = ditto.transports.read().unwrap();
assert!(!tr.ws_clients.is_empty());
}
#[test]
fn test_stop_sync() {
let ditto = get_offline_ditto().unwrap();
let mut config = TransportConfig::new();
config.peer_to_peer.bluetooth_le.enabled = true;
config.peer_to_peer.lan.enabled = true;
config
.connect
.websocket_urls
.insert(String::from("ws://ditto.com"));
ditto.set_transport_config(config);
ditto.start_sync().unwrap();
let effective_config = ditto.effective_transport_config().unwrap();
assert!(!effective_config.connect.websocket_urls.is_empty());
ditto.stop_sync();
let tr = ditto.transports.read().unwrap();
assert!(tr.ws_clients.is_empty());
}
#[test]
fn test_cloud_url_persistence() {
let ditto = get_offline_ditto().unwrap();
let mut config = TransportConfig::new();
config.peer_to_peer.bluetooth_le.enabled = true;
config
.connect
.websocket_urls
.insert(String::from("ws://ditto.com"));
ditto.set_transport_config(config);
let config = ditto.current_transport_config().unwrap();
assert!(!config.connect.websocket_urls.is_empty());
let effective_config = ditto.effective_transport_config().unwrap();
assert!(effective_config.connect.websocket_urls.is_empty());
ditto.start_sync().unwrap();
let config = ditto.current_transport_config().unwrap();
assert!(!config.connect.websocket_urls.is_empty());
let effective_config = ditto.effective_transport_config().unwrap();
assert!(!effective_config.connect.websocket_urls.is_empty());
}
}