diff options
Diffstat (limited to 'lib/src/profile/mod.rs')
-rw-r--r-- | lib/src/profile/mod.rs | 226 |
1 files changed, 226 insertions, 0 deletions
diff --git a/lib/src/profile/mod.rs b/lib/src/profile/mod.rs new file mode 100644 index 0000000..00087f3 --- /dev/null +++ b/lib/src/profile/mod.rs @@ -0,0 +1,226 @@ +use std::path::PathBuf; +use std::convert::TryInto; + +use anyhow::Context; +use anyhow::Result; + +use crate::client::Client; +use crate::ipfs_client::IpfsClient; + +mod device; +use device::Device; +mod state; +use state::*; + +#[derive(Debug, getset::Getters, getset::MutGetters)] +pub struct Profile { + state: ProfileState, + + #[getset(get = "pub", get_mut = "pub")] + client: Client, +} + +impl Profile { + pub async fn create(state_dir: &StateDir, name: &str) -> Result<Self> { + let bootstrap = vec![]; // TODO + let mdns = true; // TODO + let keypair = ipfs::Keypair::generate_ed25519(); + + let options = ipfs::IpfsOptions { + ipfs_path: Self::ipfs_path(state_dir).await?, + keypair, + bootstrap, + mdns, + kad_protocol: None, + listening_addrs: vec![], + span: Some(tracing::trace_span!("distrox-ipfs")), + }; + + let keypair = options.keypair.clone(); + let (ipfs, fut): (ipfs::Ipfs<_>, _) = ipfs::UninitializedIpfs::<_>::new(options) + .start() + .await?; + tokio::task::spawn(fut); + Self::new(ipfs, name.to_string(), keypair).await + } + + pub async fn new_inmemory(name: &str) -> Result<Self> { + let mut opts = ipfs::IpfsOptions::inmemory_with_generated_keys(); + opts.mdns = true; + let keypair = opts.keypair.clone(); + let (ipfs, fut): (ipfs::Ipfs<_>, _) = ipfs::UninitializedIpfs::<_>::new(opts).start().await.unwrap(); + tokio::task::spawn(fut); + Self::new(ipfs, format!("inmemory-{}", name), keypair).await + } + + async fn new(ipfs: IpfsClient, profile_name: String, keypair: libp2p::identity::Keypair) -> Result<Self> { + let client = Client::new(ipfs); + let state = ProfileState::new(profile_name, keypair); + Ok(Profile { state, client }) + } + + pub fn head(&self) -> Option<&cid::Cid> { + self.state.profile_head().as_ref() + } + + pub async fn listen_on(&self, addr: ipfs::Multiaddr) -> Result<()> { + self.client.listen_on(addr).await + } + + pub async fn connect(&self, peer: ipfs::MultiaddrWithPeerId) -> Result<()> { + self.client.connect(peer).await + } + + pub async fn post_text(&mut self, text: String) -> Result<cid::Cid> { + let parent = self.state + .profile_head() + .as_ref() + .map(cid::Cid::clone) + .into_iter() + .collect::<Vec<cid::Cid>>(); + + let new_cid = self.client.post_text_node(parent, text).await?; + self.state.update_head(new_cid.clone())?; + Ok(new_cid) + } + + async fn ipfs_path(state_dir: &StateDir) -> Result<PathBuf> { + let path = state_dir.ipfs(); + tokio::fs::create_dir_all(&path).await?; + Ok(path) + } + + pub fn config_path(name: &str) -> String { + format!("distrox-{}", name) + } + + pub fn config_file_path(name: &str) -> Result<PathBuf> { + xdg::BaseDirectories::with_prefix("distrox") + .map_err(anyhow::Error::from) + .and_then(|dirs| { + let name = Self::config_path(name); + dirs.place_config_file(name) + .map_err(anyhow::Error::from) + }) + } + + pub fn state_dir_path(name: &str) -> Result<StateDir> { + log::debug!("Getting state directory path"); + xdg::BaseDirectories::with_prefix("distrox") + .context("Fetching 'distrox' XDG base directory") + .map_err(anyhow::Error::from) + .and_then(|dirs| { + dirs.create_state_directory(name) + .map(StateDir::from) + .with_context(|| format!("Creating 'distrox' XDG state directory for '{}'", name)) + .map_err(anyhow::Error::from) + }) + } + + pub async fn save(&self) -> Result<()> { + let state_dir_path = Self::state_dir_path(self.state.profile_name())?; + log::trace!("Saving to {:?}", state_dir_path.display()); + ProfileStateSaveable::new(&self.state) + .context("Serializing profile state")? + .save_to_disk(&state_dir_path) + .await + .context("Saving state to disk") + .map_err(anyhow::Error::from) + } + + pub async fn load(name: &str) -> Result<Self> { + let state_dir_path = Self::state_dir_path(name)?; + log::trace!("state_dir_path = {:?}", state_dir_path.display()); + let state: ProfileState = ProfileStateSaveable::load_from_disk(&state_dir_path) + .await? + .try_into() + .context("Parsing profile state")?; + log::debug!("Loading state finished"); + + let bootstrap = vec![]; // TODO + let mdns = true; // TODO + let keypair = state.keypair().clone(); + + log::debug!("Configuring IPFS backend"); + let options = ipfs::IpfsOptions { + ipfs_path: Self::ipfs_path(&state_dir_path).await?, + keypair, + bootstrap, + mdns, + kad_protocol: None, + listening_addrs: vec![], + span: Some(tracing::trace_span!("distrox-ipfs")), + }; + + log::debug!("Starting IPFS backend"); + let (ipfs, fut): (ipfs::Ipfs<_>, _) = ipfs::UninitializedIpfs::<_>::new(options) + .start() + .await?; + tokio::task::spawn(fut); + + log::debug!("Profile loading finished"); + Ok(Profile { + state, + client: Client::new(ipfs), + }) + } + + pub async fn exit(self) -> Result<()> { + self.client.exit().await + } + + + pub fn add_device(&mut self, d: Device) -> Result<()> { + self.state.add_device(d) + } + + pub async fn gossip_own_state(&self, topic: String) -> Result<()> { + let cid = self.state + .profile_head() + .as_ref() + .ok_or_else(|| anyhow::anyhow!("Profile has no HEAD yet"))? + .to_bytes(); + + let peer_id = self.client + .own_id() + .await? + .to_bytes(); + + self.client + .ipfs + .pubsub_publish(topic, { + crate::gossip::GossipMessage::CurrentProfileState { + peer_id, + cid, + }.into_bytes()? + }) + .await + .map_err(anyhow::Error::from) + } +} + + +#[cfg(test)] +mod tests { + use super::*; + use std::convert::TryFrom; + + #[tokio::test] + async fn test_create_profile() { + let _ = env_logger::try_init(); + let profile = Profile::new_inmemory("test-create-profile").await; + assert!(profile.is_ok()); + let exit = profile.unwrap().exit().await; + assert!(exit.is_ok(), "Not cleanly exited: {:?}", exit); + } + + #[tokio::test] + async fn test_create_profile_and_helloworld() { + let _ = env_logger::try_init(); + let profile = Profile::new_inmemory("test-create-profile-and-helloworld").await; + assert!(profile.is_ok()); + let profile = profile.unwrap(); + assert!(profile.head().is_none()); + } + +} |