im-core 0.1.0

Rust IM SDK for Awiki clients built on Agent Network Protocol (ANP)
Documentation
use std::sync::Arc;

#[cfg(feature = "sqlite")]
use tokio::sync::OnceCell;

mod bootstrap;
mod client;
pub(crate) mod options;

pub use self::bootstrap::{
    CoreBootstrap, LocalStateStatus, MigrationReport, PathCheck, PathValidationReport,
};
pub use self::client::ImClient;
pub use self::options::{IdentitySecretStoragePolicy, ImCoreOpenOptions, ImCoreSecretVaultOptions};

pub(crate) struct ImCoreInner {
    pub(crate) sdk_config: crate::ImCoreConfig,
    pub(crate) sdk_paths: crate::ImCorePaths,
    pub(crate) identity_secret_storage_policy: IdentitySecretStoragePolicy,
    pub(crate) identity_vault: Option<options::IdentityVaultContext>,
    #[cfg(feature = "sqlite")]
    pub(crate) local_state_db: OnceCell<crate::internal::local_state::actor::LocalStateDb>,
}

#[derive(Clone)]
pub struct ImCore {
    inner: Arc<ImCoreInner>,
}

impl ImCore {
    pub async fn open(
        sdk_config: crate::ImCoreConfig,
        sdk_paths: crate::ImCorePaths,
    ) -> crate::ImResult<Self> {
        Self::new(sdk_config, sdk_paths)
    }

    pub async fn open_with_options(
        sdk_config: crate::ImCoreConfig,
        sdk_paths: crate::ImCorePaths,
        options: ImCoreOpenOptions,
    ) -> crate::ImResult<Self> {
        Self::new_with_options(sdk_config, sdk_paths, options)
    }

    pub fn new(
        sdk_config: crate::ImCoreConfig,
        sdk_paths: crate::ImCorePaths,
    ) -> crate::ImResult<Self> {
        Self::new_with_options(sdk_config, sdk_paths, ImCoreOpenOptions::default())
    }

    pub fn new_with_options(
        sdk_config: crate::ImCoreConfig,
        sdk_paths: crate::ImCorePaths,
        options: ImCoreOpenOptions,
    ) -> crate::ImResult<Self> {
        if sdk_config.did_domain.trim().is_empty() {
            return Err(crate::ImError::invalid_input(
                Some("did_domain".to_string()),
                "DID domain must not be empty",
            ));
        }
        let identity_vault = options
            .identity_secret_vault
            .map(options::IdentityVaultContext::from_options)
            .transpose()?
            .map(|context| context.with_policy(options.identity_secret_storage_policy));
        if matches!(
            options.identity_secret_storage_policy,
            IdentitySecretStoragePolicy::VaultRequired
        ) && identity_vault.is_none()
        {
            return Err(crate::ImError::LocalStateUnavailable {
                detail: "identity secret storage policy is VaultRequired but no identity secret vault was provided".to_owned(),
            });
        }
        Ok(Self {
            inner: Arc::new(ImCoreInner {
                sdk_config,
                sdk_paths,
                identity_secret_storage_policy: options.identity_secret_storage_policy,
                identity_vault,
                #[cfg(feature = "sqlite")]
                local_state_db: OnceCell::new(),
            }),
        })
    }

    pub fn identities(&self) -> crate::identity::IdentityRegistry<'_> {
        crate::identity::IdentityRegistry::new(self)
    }

    pub fn bootstrap(&self) -> CoreBootstrap<'_> {
        CoreBootstrap::new(self)
    }

    pub fn client(&self, selector: crate::identity::IdentitySelector) -> crate::ImResult<ImClient> {
        let runtime = self.identities().load_runtime(selector)?;
        Ok(ImClient::new(self.inner.clone(), runtime))
    }

    pub async fn client_async(
        &self,
        selector: crate::identity::IdentitySelector,
    ) -> crate::ImResult<ImClient> {
        let runtime = self.identities().load_runtime_async(selector).await?;
        Ok(ImClient::new(self.inner.clone(), runtime))
    }

    pub fn client_with_identity_material(
        &self,
        material: crate::identity::HostedIdentityMaterial,
    ) -> crate::ImResult<ImClient> {
        let identity_id = crate::ids::IdentityId::parse(&material.identity_id)?;
        let did = crate::ids::Did::parse(&material.did)?;
        let handle = material
            .handle
            .as_deref()
            .map(|handle| crate::ids::Handle::parse(handle, &self.inner.sdk_config().did_domain))
            .transpose()?;
        let key_provider = std::sync::Arc::new(
            crate::internal::key_provider::HostedKeyMaterialProvider::new(&material)?,
        );
        let runtime = crate::internal::identity_runtime::ClientIdentityRuntime {
            summary: crate::identity::IdentitySummary {
                id: identity_id.clone(),
                did: did.clone(),
                handle,
                display_name: material.display_name,
                local_alias: None,
                device_id: None,
                is_default: false,
                readiness: crate::identity::IdentityReadiness {
                    ready_for_auth: true,
                    ready_for_messaging: true,
                    missing: Vec::new(),
                },
            },
            did_document_path: std::path::PathBuf::new(),
            private_key_path: std::path::PathBuf::new(),
            e2ee_agreement_private_key_path: std::path::PathBuf::new(),
            auth_state_path: std::path::PathBuf::new(),
            key_provider,
            owner: crate::internal::identity_runtime::LocalOwnerContext {
                identity_id,
                current_did: did,
            },
        };
        Ok(ImClient::new(self.inner.clone(), runtime))
    }

    pub(crate) fn inner(&self) -> &ImCoreInner {
        &self.inner
    }
}

impl ImCoreInner {
    pub(crate) fn sdk_config(&self) -> &crate::ImCoreConfig {
        &self.sdk_config
    }

    pub(crate) fn sdk_paths(&self) -> &crate::ImCorePaths {
        &self.sdk_paths
    }

    pub(crate) fn identity_secret_storage_policy(&self) -> IdentitySecretStoragePolicy {
        self.identity_secret_storage_policy
    }

    pub(crate) fn identity_vault(&self) -> Option<&options::IdentityVaultContext> {
        self.identity_vault.as_ref()
    }

    #[cfg(feature = "sqlite")]
    pub(crate) async fn local_state_db(
        &self,
    ) -> crate::ImResult<crate::internal::local_state::actor::LocalStateDb> {
        self.local_state_db
            .get_or_try_init(|| {
                crate::internal::local_state::actor::LocalStateDb::open(
                    self.sdk_paths.local_state.sqlite_path.clone(),
                )
            })
            .await
            .cloned()
    }
}

#[cfg(all(test, feature = "sqlite"))]
mod tests {
    use super::*;

    #[tokio::test]
    async fn local_state_db_concurrent_first_open_shares_actor() {
        let root = tempfile::tempdir().unwrap();
        let core = ImCore::new(
            crate::ImCoreConfig {
                service_base_url: crate::ServiceEndpoint::parse("https://example.test").unwrap(),
                did_domain: "awiki.test".to_owned(),
                user_service_endpoint: None,
                message_service_endpoint: None,
                mail_service_endpoint: None,
                anp_service_endpoint: None,
                anp_service_did: None,
                ca_bundle: None,
                transport_policy: crate::MessageTransportPolicy::HttpOnly,
            },
            crate::ImCorePaths {
                identities: crate::IdentityRegistryPaths {
                    identity_root_dir: root.path().join("identities"),
                    registry_path: root.path().join("identities").join("registry.json"),
                    default_identity_path: Some(root.path().join("identities").join("default")),
                },
                local_state: crate::LocalStatePaths {
                    sqlite_path: root.path().join("local").join("im.sqlite"),
                },
                runtime: crate::RuntimePaths {
                    cache_dir: root.path().join("cache"),
                    temp_dir: root.path().join("tmp"),
                },
            },
        )
        .unwrap();

        let inner = core.inner.clone();
        let mut tasks = Vec::new();
        for _ in 0..8 {
            let inner = inner.clone();
            tasks.push(tokio::spawn(async move {
                let db = inner.local_state_db().await.unwrap();
                db.current_schema_version().await.unwrap()
            }));
        }

        for task in tasks {
            assert_eq!(
                task.await.unwrap(),
                crate::internal::local_state::schema::SCHEMA_VERSION
            );
        }
        assert!(core.inner().local_state_db.get().is_some());
    }
}