From 7f253887d9d594fc7c2afdc001395c7848d49a16 Mon Sep 17 00:00:00 2001 From: Matthias Beyer Date: Thu, 2 Dec 2021 20:10:33 +0100 Subject: Add dev dependencies Signed-off-by: Matthias Beyer --- shell.nix | 3 +++ 1 file changed, 3 insertions(+) diff --git a/shell.nix b/shell.nix index e659c40..8abcf47 100644 --- a/shell.nix +++ b/shell.nix @@ -22,6 +22,9 @@ let pkgconfig which zlib + + freetype + expat ]; in -- cgit v1.2.3 From 969de98ddad7311fbcd74f93910444af8fdb7882 Mon Sep 17 00:00:00 2001 From: Matthias Beyer Date: Thu, 2 Dec 2021 20:32:43 +0100 Subject: Add xorg dependencies in dev shell Signed-off-by: Matthias Beyer --- shell.nix | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/shell.nix b/shell.nix index 8abcf47..32a0774 100644 --- a/shell.nix +++ b/shell.nix @@ -26,10 +26,33 @@ let freetype expat ]; + xorgPackages = with pkgs.xorg; [ + libXcursor + libXfont2 + # libXpm + # libXtst + # libxshmfence + # libXft + libXrandr + libXext + # libXinerama + # libXrender + # libXxf86misc + # libxcb + libX11 + # libXcomposite + libXfont + libXi + # libXt + # libxkbfile + + pkgs.libGL + ]; in pkgs.mkShell rec { - buildInputs = env ++ dependencies; + buildInputs = env ++ dependencies ++ xorgPackages; LIBCLANG_PATH = "${pkgs.llvmPackages.libclang}/lib"; PROTOC = "${pkgs.protobuf}/bin/protoc"; + LD_LIBRARY_PATH = pkgs.lib.makeLibraryPath xorgPackages; } -- cgit v1.2.3 From 36dc8675f5757c81d3b78da19d6f7bb4f34a555c Mon Sep 17 00:00:00 2001 From: Matthias Beyer Date: Thu, 2 Dec 2021 19:57:16 +0100 Subject: Implement hello-world gui with iced Signed-off-by: Matthias Beyer --- Cargo.toml | 6 ++++++ src/cli.rs | 7 ++++++- src/gui/mod.rs | 32 ++++++++++++++++++++++++++++++++ src/main.rs | 17 ++++++++++++----- 4 files changed, 56 insertions(+), 6 deletions(-) create mode 100644 src/gui/mod.rs diff --git a/Cargo.toml b/Cargo.toml index 6766fe6..1760305 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -39,10 +39,16 @@ libp2p = "0.39.1" tracing = "0.1" ctrlc = "3.2" +iced_native = "0.4.0" +iced_wgpu = "0.4.0" + [dependencies.ipfs] git = "https://github.com/rs-ipfs/rust-ipfs/" rev = "ad3ab49b4d9236363969b0f74f14aabc7c906b3b" +[dependencies.iced] +version = "0.3.0" +features = ["glow", "tokio", "debug"] [dev-dependencies] multibase = "0.8" diff --git a/src/cli.rs b/src/cli.rs index 8e912e0..3f74128 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -9,7 +9,6 @@ pub fn app<'a>() -> App<'a> { .version(crate_version!()) .about("Distributed social network") - .subcommand(App::new("profile") .author(crate_authors!()) .version(crate_version!()) @@ -51,4 +50,10 @@ pub fn app<'a>() -> App<'a> { ) ) ) + + .subcommand(App::new("gui") + .author(crate_authors!()) + .version(crate_version!()) + .about("Start the distrox gui") + ) } diff --git a/src/gui/mod.rs b/src/gui/mod.rs new file mode 100644 index 0000000..a7cc4ff --- /dev/null +++ b/src/gui/mod.rs @@ -0,0 +1,32 @@ +use anyhow::Result; +use iced::Application; + +#[derive(Debug)] +struct DistroxGui; + +impl Application for DistroxGui { + type Executor = iced::executor::Default; // tokio + type Message = (); + type Flags = (); + + fn new(_flags: ()) -> (Self, iced::Command) { + (DistroxGui, iced::Command::none()) + } + + fn title(&self) -> String { + String::from("distrox") + } + + fn update(&mut self, _message: Self::Message, _clipboard: &mut iced::Clipboard) -> iced::Command { + iced::Command::none() + } + + fn view(&mut self) -> iced::Element { + iced::Text::new("Hello, world!").into() + } + +} + +pub fn run() -> Result<()> { + DistroxGui::run(iced::Settings::default()).map_err(anyhow::Error::from) +} diff --git a/src/main.rs b/src/main.rs index 257847c..3caf1ae 100644 --- a/src/main.rs +++ b/src/main.rs @@ -8,17 +8,24 @@ pub mod consts; pub mod ipfs_client; pub mod profile; pub mod types; +mod gui; -#[tokio::main] -async fn main() -> Result<()> { +fn main() -> Result<()> { let _ = env_logger::try_init()?; let matches = crate::cli::app().get_matches(); match matches.subcommand() { Some(("profile", matches)) => crate::commands::profile(matches).await, - _ => unimplemented!() - } - + Some(("gui", _)) => crate::gui::run(), + Some((other, _)) => { + log::error!("No subcommand {} implemented", other); + Ok(()) + }, + _ => { + log::error!("Don't know what to do"); + Ok(()) + }, + } } -- cgit v1.2.3 From 59cfb079b7ce09dbefb5e67791e62d746dd47ecb Mon Sep 17 00:00:00 2001 From: Matthias Beyer Date: Thu, 2 Dec 2021 21:05:07 +0100 Subject: Add some default settings for application window Signed-off-by: Matthias Beyer --- src/gui/mod.rs | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/gui/mod.rs b/src/gui/mod.rs index a7cc4ff..4574a39 100644 --- a/src/gui/mod.rs +++ b/src/gui/mod.rs @@ -28,5 +28,17 @@ impl Application for DistroxGui { } pub fn run() -> Result<()> { - DistroxGui::run(iced::Settings::default()).map_err(anyhow::Error::from) + let settings = iced::Settings { + window: iced::window::Settings { + resizable: true, + decorations: true, + transparent: false, + always_on_top: false, + ..iced::window::Settings::default() + }, + exit_on_close_request: true, + ..iced::Settings::default() + }; + + DistroxGui::run(settings).map_err(anyhow::Error::from) } -- cgit v1.2.3 From 08c400b793ecfc12b462cac91ee37c780179f5f6 Mon Sep 17 00:00:00 2001 From: Matthias Beyer Date: Thu, 2 Dec 2021 21:05:17 +0100 Subject: Add loading of client connection Signed-off-by: Matthias Beyer --- src/gui/mod.rs | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/src/gui/mod.rs b/src/gui/mod.rs index 4574a39..97751b0 100644 --- a/src/gui/mod.rs +++ b/src/gui/mod.rs @@ -1,16 +1,34 @@ use anyhow::Result; use iced::Application; +use ipfs_api_backend_hyper::TryFromUri; + +use crate::client::Client; +use crate::config::Config; +use crate::ipfs_client::IpfsClient; #[derive(Debug)] struct DistroxGui; +#[derive(Debug)] +enum Message { + Loaded(Result), +} + impl Application for DistroxGui { type Executor = iced::executor::Default; // tokio - type Message = (); + type Message = Message; type Flags = (); fn new(_flags: ()) -> (Self, iced::Command) { - (DistroxGui, iced::Command::none()) + ( + DistroxGui, + iced::Command::perform(async { + let ipfs = IpfsClient::from_str("http://localhost:5001")?; + let config = Config::default(); + let client = Client::new(ipfs, config); + Ok(client) + }, Message::Loaded) + ) } fn title(&self) -> String { -- cgit v1.2.3 From 3b287d4548a9125f3fdcb6e15ea8d85b1167c21a Mon Sep 17 00:00:00 2001 From: Matthias Beyer Date: Thu, 2 Dec 2021 21:45:29 +0100 Subject: Add simple posting GUI implementation Signed-off-by: Matthias Beyer --- src/gui/mod.rs | 140 ++++++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 128 insertions(+), 12 deletions(-) diff --git a/src/gui/mod.rs b/src/gui/mod.rs index 97751b0..584392e 100644 --- a/src/gui/mod.rs +++ b/src/gui/mod.rs @@ -1,5 +1,15 @@ +use std::sync::Arc; + use anyhow::Result; use iced::Application; +use iced::Column; +use iced::Container; +use iced::Element; +use iced::Length; +use iced::Scrollable; +use iced::TextInput; +use iced::scrollable; +use iced::text_input; use ipfs_api_backend_hyper::TryFromUri; use crate::client::Client; @@ -7,27 +17,51 @@ use crate::config::Config; use crate::ipfs_client::IpfsClient; #[derive(Debug)] -struct DistroxGui; +enum Distrox { + Loading, + Loaded(State), + FailedToStart, +} #[derive(Debug)] +struct State { + client: Arc, + + scroll: scrollable::State, + input: text_input::State, + input_value: String, +} + +#[derive(Debug, Clone)] enum Message { - Loaded(Result), + Loaded(Arc), + FailedToLoad, + + InputChanged(String), + CreatePost, + + PostCreated(crate::cid::Cid), + PostCreationFailed(String), } -impl Application for DistroxGui { +impl Application for Distrox { type Executor = iced::executor::Default; // tokio type Message = Message; type Flags = (); fn new(_flags: ()) -> (Self, iced::Command) { ( - DistroxGui, + Distrox::Loading, iced::Command::perform(async { - let ipfs = IpfsClient::from_str("http://localhost:5001")?; - let config = Config::default(); - let client = Client::new(ipfs, config); - Ok(client) - }, Message::Loaded) + match IpfsClient::from_str("http://localhost:5001") { + Err(_) => Message::FailedToLoad, + Ok(ipfs) => { + let config = Config::default(); + let client = Client::new(ipfs, config); + Message::Loaded(Arc::new(client)) + } + } + }, |m: Message| -> Message { m }) ) } @@ -35,12 +69,94 @@ impl Application for DistroxGui { String::from("distrox") } - fn update(&mut self, _message: Self::Message, _clipboard: &mut iced::Clipboard) -> iced::Command { + fn update(&mut self, message: Self::Message, _clipboard: &mut iced::Clipboard) -> iced::Command { + match self { + Distrox::Loading => { + match message { + Message::Loaded(client) => { + let state = State { + client: client, + scroll: scrollable::State::default(), + input: text_input::State::default(), + input_value: String::default(), + }; + *self = Distrox::Loaded(state); + } + + Message::FailedToLoad => { + log::error!("Failed to load"); + *self = Distrox::FailedToStart; + } + + _ => {} + + } + } + + Distrox::Loaded(state) => { + match message { + Message::InputChanged(input) => { + state.input_value = input; + } + + Message::CreatePost => { + if !state.input_value.is_empty() { + let client = state.client.clone(); + iced::Command::perform(async move { + client.post_text_blob(state.input_value.clone()).await + }, + |res| match res { + Ok(cid) => Message::PostCreated(cid), + Err(e) => Message::PostCreationFailed(e.to_string()) + }); + } + } + + _ => {} + } + } + + Distrox::FailedToStart => { + unimplemented!() + } + } iced::Command::none() } fn view(&mut self) -> iced::Element { - iced::Text::new("Hello, world!").into() + match self { + Distrox::Loading => { + unimplemented!() + } + + Distrox::Loaded(state) => { + let input = TextInput::new( + &mut state.input, + "What do you want to tell the world?", + &mut state.input_value, + Message::InputChanged, + ) + .padding(15) + .size(30) + .on_submit(Message::CreatePost); + + let content = Column::new() + .max_width(800) + .spacing(20) + .push(input); + + Scrollable::new(&mut state.scroll) + .padding(40) + .push( + Container::new(content).width(Length::Fill).center_x(), + ) + .into() + } + + Distrox::FailedToStart => { + unimplemented!() + } + } } } @@ -58,5 +174,5 @@ pub fn run() -> Result<()> { ..iced::Settings::default() }; - DistroxGui::run(settings).map_err(anyhow::Error::from) + Distrox::run(settings).map_err(anyhow::Error::from) } -- cgit v1.2.3 From 9482b10293f7e2b21647e986304f32ff7795559e Mon Sep 17 00:00:00 2001 From: Matthias Beyer Date: Sat, 4 Dec 2021 21:33:41 +0100 Subject: Store Profile in GUI instead of Client Signed-off-by: Matthias Beyer --- Cargo.toml | 1 - src/gui/mod.rs | 26 ++++++++++++-------------- 2 files changed, 12 insertions(+), 15 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 1760305..d8a78c8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,7 +35,6 @@ serde = "1" serde_json = "1" getset = "0.1" xdg = "2.4" -libp2p = "0.39.1" tracing = "0.1" ctrlc = "3.2" diff --git a/src/gui/mod.rs b/src/gui/mod.rs index 584392e..bc7c9e9 100644 --- a/src/gui/mod.rs +++ b/src/gui/mod.rs @@ -10,9 +10,8 @@ use iced::Scrollable; use iced::TextInput; use iced::scrollable; use iced::text_input; -use ipfs_api_backend_hyper::TryFromUri; -use crate::client::Client; +use crate::profile::Profile; use crate::config::Config; use crate::ipfs_client::IpfsClient; @@ -25,7 +24,7 @@ enum Distrox { #[derive(Debug)] struct State { - client: Arc, + profile: Arc, scroll: scrollable::State, input: text_input::State, @@ -34,13 +33,13 @@ struct State { #[derive(Debug, Clone)] enum Message { - Loaded(Arc), + Loaded(Arc), FailedToLoad, InputChanged(String), CreatePost, - PostCreated(crate::cid::Cid), + PostCreated(cid::Cid), PostCreationFailed(String), } @@ -53,12 +52,10 @@ impl Application for Distrox { ( Distrox::Loading, iced::Command::perform(async { - match IpfsClient::from_str("http://localhost:5001") { + match Profile::new_inmemory(Config::default()).await { Err(_) => Message::FailedToLoad, - Ok(ipfs) => { - let config = Config::default(); - let client = Client::new(ipfs, config); - Message::Loaded(Arc::new(client)) + Ok(instance) => { + Message::Loaded(Arc::new(instance)) } } }, |m: Message| -> Message { m }) @@ -73,9 +70,9 @@ impl Application for Distrox { match self { Distrox::Loading => { match message { - Message::Loaded(client) => { + Message::Loaded(profile) => { let state = State { - client: client, + profile, scroll: scrollable::State::default(), input: text_input::State::default(), input_value: String::default(), @@ -101,9 +98,10 @@ impl Application for Distrox { Message::CreatePost => { if !state.input_value.is_empty() { - let client = state.client.clone(); + let profile = state.profile.clone(); + let input = state.input_value.clone(); iced::Command::perform(async move { - client.post_text_blob(state.input_value.clone()).await + profile.client().post_text_blob(input).await }, |res| match res { Ok(cid) => Message::PostCreated(cid), -- cgit v1.2.3 From ffe02288fb2390df9194ad660293de3b4144c3ab Mon Sep 17 00:00:00 2001 From: Matthias Beyer Date: Sat, 4 Dec 2021 21:38:51 +0100 Subject: Add "Loading" text block Signed-off-by: Matthias Beyer --- src/gui/mod.rs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/gui/mod.rs b/src/gui/mod.rs index bc7c9e9..503ed9e 100644 --- a/src/gui/mod.rs +++ b/src/gui/mod.rs @@ -124,7 +124,17 @@ impl Application for Distrox { fn view(&mut self) -> iced::Element { match self { Distrox::Loading => { - unimplemented!() + let text = iced_native::widget::text::Text::new("Loading"); + + let content = Column::new() + .max_width(800) + .spacing(20) + .push(text); + + Container::new(content) + .width(Length::Fill) + .center_x() + .into() } Distrox::Loaded(state) => { -- cgit v1.2.3 From 82add278cc59329345c49554539922926635ee70 Mon Sep 17 00:00:00 2001 From: Matthias Beyer Date: Tue, 7 Dec 2021 13:19:32 +0100 Subject: Add getter for Client Signed-off-by: Matthias Beyer --- src/profile/mod.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/profile/mod.rs b/src/profile/mod.rs index 56b4ac9..e3ce175 100644 --- a/src/profile/mod.rs +++ b/src/profile/mod.rs @@ -11,9 +11,11 @@ use crate::ipfs_client::IpfsClient; mod state; use state::*; -#[derive(Debug)] +#[derive(Debug, getset::Getters)] pub struct Profile { state: ProfileState, + + #[getset(get = "pub")] client: Client, } -- cgit v1.2.3 From 7ae0325d7e54df8db88bc7bc757c3cb674a0fb0f Mon Sep 17 00:00:00 2001 From: Matthias Beyer Date: Wed, 8 Dec 2021 17:50:29 +0100 Subject: Split codebase in subcrates for lib, cli and gui Signed-off-by: Matthias Beyer --- Cargo.toml | 59 +-------- cli/Cargo.toml | 49 +++++++ cli/src/cli.rs | 59 +++++++++ cli/src/commands/mod.rs | 2 + cli/src/commands/profile.rs | 67 ++++++++++ cli/src/main.rs | 27 ++++ gui/Cargo.toml | 44 +++++++ gui/src/cli.rs | 12 ++ gui/src/gui/mod.rs | 186 ++++++++++++++++++++++++++ gui/src/main.rs | 26 ++++ lib/Cargo.toml | 47 +++++++ lib/src/cid.rs | 54 ++++++++ lib/src/client.rs | 315 ++++++++++++++++++++++++++++++++++++++++++++ lib/src/config.rs | 10 ++ lib/src/consts.rs | 4 + lib/src/ipfs_client.rs | 5 + lib/src/lib.rs | 6 + lib/src/profile/mod.rs | 182 +++++++++++++++++++++++++ lib/src/profile/state.rs | 129 ++++++++++++++++++ lib/src/types/datetime.rs | 35 +++++ lib/src/types/mod.rs | 8 ++ lib/src/types/node.rs | 88 +++++++++++++ lib/src/types/payload.rs | 72 ++++++++++ src/cid.rs | 54 -------- src/cli.rs | 59 --------- src/client.rs | 315 -------------------------------------------- src/commands/mod.rs | 2 - src/commands/profile.rs | 67 ---------- src/config.rs | 10 -- src/consts.rs | 4 - src/gui/mod.rs | 186 -------------------------- src/ipfs_client.rs | 5 - src/main.rs | 31 ----- src/profile/mod.rs | 182 ------------------------- src/profile/state.rs | 129 ------------------ src/types/datetime.rs | 35 ----- src/types/mod.rs | 8 -- src/types/node.rs | 88 ------------- src/types/payload.rs | 72 ---------- 39 files changed, 1433 insertions(+), 1300 deletions(-) create mode 100644 cli/Cargo.toml create mode 100644 cli/src/cli.rs create mode 100644 cli/src/commands/mod.rs create mode 100644 cli/src/commands/profile.rs create mode 100644 cli/src/main.rs create mode 100644 gui/Cargo.toml create mode 100644 gui/src/cli.rs create mode 100644 gui/src/gui/mod.rs create mode 100644 gui/src/main.rs create mode 100644 lib/Cargo.toml create mode 100644 lib/src/cid.rs create mode 100644 lib/src/client.rs create mode 100644 lib/src/config.rs create mode 100644 lib/src/consts.rs create mode 100644 lib/src/ipfs_client.rs create mode 100644 lib/src/lib.rs create mode 100644 lib/src/profile/mod.rs create mode 100644 lib/src/profile/state.rs create mode 100644 lib/src/types/datetime.rs create mode 100644 lib/src/types/mod.rs create mode 100644 lib/src/types/node.rs create mode 100644 lib/src/types/payload.rs delete mode 100644 src/cid.rs delete mode 100644 src/cli.rs delete mode 100644 src/client.rs delete mode 100644 src/commands/mod.rs delete mode 100644 src/commands/profile.rs delete mode 100644 src/config.rs delete mode 100644 src/consts.rs delete mode 100644 src/gui/mod.rs delete mode 100644 src/ipfs_client.rs delete mode 100644 src/main.rs delete mode 100644 src/profile/mod.rs delete mode 100644 src/profile/state.rs delete mode 100644 src/types/datetime.rs delete mode 100644 src/types/mod.rs delete mode 100644 src/types/node.rs delete mode 100644 src/types/payload.rs diff --git a/Cargo.toml b/Cargo.toml index d8a78c8..f0e2060 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,53 +1,6 @@ -[package] -name = "distrox" -version = "0.1.0" -authors = ["Matthias Beyer "] - -description = "Distributed network build on IPFS" - -keywords = ["social", "network", "ipfs", "distributed"] -readme = "README.md" -license = "GPL-2.0" - -documentation = "https://docs.rs/distrox" -repository = "https://github.com/matthiasbeyer/distrox" -homepage = "http://github.com/matthiasbeyer/distrox" - -edition = "2018" - -[dependencies] -anyhow = "1" -async-trait = "0.1" -chrono = { version = "0.4", features = ["serde"] } -cid = "0.5" -clap = "3.0.0-beta.5" -daglib = { git = "https://git.sr.ht/~matthiasbeyer/daglib", branch = "master" } -env_logger = "0.8" -futures = "0.3" -log = "0.4" -tokio = { version = "1", features = ["full", "rt", "macros"] } -mime = "0.3" -rand_core = { version = "0.6", features = ["getrandom"] } -rand_os = "0.2" -ed25519-dalek = "*" -http = "0.2" -serde = "1" -serde_json = "1" -getset = "0.1" -xdg = "2.4" -tracing = "0.1" -ctrlc = "3.2" - -iced_native = "0.4.0" -iced_wgpu = "0.4.0" - -[dependencies.ipfs] -git = "https://github.com/rs-ipfs/rust-ipfs/" -rev = "ad3ab49b4d9236363969b0f74f14aabc7c906b3b" - -[dependencies.iced] -version = "0.3.0" -features = ["glow", "tokio", "debug"] - -[dev-dependencies] -multibase = "0.8" +[workspace] +members = [ + "cli", + "gui", + "lib", +] diff --git a/cli/Cargo.toml b/cli/Cargo.toml new file mode 100644 index 0000000..e558c19 --- /dev/null +++ b/cli/Cargo.toml @@ -0,0 +1,49 @@ +[package] +name = "distrox-cli" +version = "0.1.0" +authors = ["Matthias Beyer "] + +description = "Distributed network build on IPFS, CLI frontend" + +keywords = ["social", "network", "ipfs", "distributed"] +readme = "README.md" +license = "GPL-2.0" + +documentation = "https://docs.rs/distrox" +repository = "https://github.com/matthiasbeyer/distrox" +homepage = "http://github.com/matthiasbeyer/distrox" + +edition = "2018" + +[dependencies] +anyhow = "1" +async-trait = "0.1" +chrono = { version = "0.4", features = ["serde"] } +cid = "0.5" +clap = "3.0.0-beta.5" +daglib = { git = "https://git.sr.ht/~matthiasbeyer/daglib", branch = "master" } +env_logger = "0.8" +futures = "0.3" +log = "0.4" +tokio = { version = "1", features = ["full", "rt", "macros"] } +mime = "0.3" +rand_core = { version = "0.6", features = ["getrandom"] } +rand_os = "0.2" +ed25519-dalek = "*" +http = "0.2" +serde = "1" +serde_json = "1" +getset = "0.1" +xdg = "2.4" +tracing = "0.1" +ctrlc = "3.2" + +[dependencies.ipfs] +git = "https://github.com/rs-ipfs/rust-ipfs/" +rev = "ad3ab49b4d9236363969b0f74f14aabc7c906b3b" + +[dependencies.distrox-lib] +path = "../lib" + +[dev-dependencies] +multibase = "0.8" diff --git a/cli/src/cli.rs b/cli/src/cli.rs new file mode 100644 index 0000000..3f74128 --- /dev/null +++ b/cli/src/cli.rs @@ -0,0 +1,59 @@ +use clap::crate_authors; +use clap::crate_version; +use clap::App; +use clap::Arg; + +pub fn app<'a>() -> App<'a> { + App::new("distrox") + .author(crate_authors!()) + .version(crate_version!()) + .about("Distributed social network") + + .subcommand(App::new("profile") + .author(crate_authors!()) + .version(crate_version!()) + .about("Profile actions") + + .subcommand(App::new("create") + .author(crate_authors!()) + .version(crate_version!()) + .about("Create profile") + + .arg(Arg::new("name") + .long("name") + .required(true) + .takes_value(true) + .value_name("NAME") + .about("Name of the profile") + ) + ) + + .subcommand(App::new("serve") + .author(crate_authors!()) + .version(crate_version!()) + .about("Just serve the profile") + + .arg(Arg::new("name") + .long("name") + .required(true) + .takes_value(true) + .value_name("NAME") + .about("Name of the profile") + ) + + .arg(Arg::new("connect") + .long("connect") + .required(false) + .takes_value(true) + .value_name("MULTIADDR") + .about("Connect to MULTIADDR as well") + ) + ) + ) + + .subcommand(App::new("gui") + .author(crate_authors!()) + .version(crate_version!()) + .about("Start the distrox gui") + ) +} diff --git a/cli/src/commands/mod.rs b/cli/src/commands/mod.rs new file mode 100644 index 0000000..5569bcb --- /dev/null +++ b/cli/src/commands/mod.rs @@ -0,0 +1,2 @@ +mod profile; +pub use profile::profile; diff --git a/cli/src/commands/profile.rs b/cli/src/commands/profile.rs new file mode 100644 index 0000000..0eb8b75 --- /dev/null +++ b/cli/src/commands/profile.rs @@ -0,0 +1,67 @@ +use std::sync::atomic::AtomicBool; +use std::sync::atomic::Ordering; +use std::sync::Arc; + +use anyhow::Context; +use anyhow::Result; +use clap::ArgMatches; + +use distrox_lib::config::Config; +use distrox_lib::profile::Profile; + +pub async fn profile(matches: &ArgMatches) -> Result<()> { + match matches.subcommand() { + Some(("create", m)) => profile_create(m).await, + Some(("serve", m)) => profile_serve(m).await, + _ => unimplemented!(), + } +} + +async fn profile_create(matches: &ArgMatches) -> Result<()> { + let name = matches.value_of("name").map(String::from).unwrap(); // required + let state_dir = Profile::state_dir_path(&name)?; + log::info!("Creating '{}' in {}", name, state_dir.display()); + + let profile = Profile::create(&state_dir, &name, Config::default()).await?; + log::info!("Saving..."); + profile.save().await?; + + log::info!("Shutting down..."); + profile.exit().await +} + +async fn profile_serve(matches: &ArgMatches) -> Result<()> { + use ipfs::MultiaddrWithPeerId; + + let name = matches.value_of("name").map(String::from).unwrap(); // required + let connect_peer = matches.value_of("connect").map(|s| { + s.parse::() + .map_err(anyhow::Error::from) + }).transpose()?; + + let state_dir = Profile::state_dir_path(&name)?; + + log::info!("Loading '{}' from {}", name, state_dir.display()); + let profile = Profile::load(Config::default(), &name).await?; + log::info!("Profile loaded"); + log::info!("Profile HEAD = {:?}", profile.head()); + + if let Some(connect_to) = connect_peer { + log::info!("Connecting to {:?}", connect_to); + profile.connect(connect_to).await?; + } + + let running = Arc::new(AtomicBool::new(true)); + let r = running.clone(); + + ctrlc::set_handler(move || { + r.store(false, Ordering::SeqCst); + }).context("Error setting Ctrl-C handler")?; + + log::info!("Serving..."); + while running.load(Ordering::SeqCst) { + tokio::time::sleep(std::time::Duration::from_millis(500)).await // sleep not so busy + } + log::info!("Shutting down..."); + profile.exit().await +} diff --git a/cli/src/main.rs b/cli/src/main.rs new file mode 100644 index 0000000..fea466c --- /dev/null +++ b/cli/src/main.rs @@ -0,0 +1,27 @@ +use anyhow::Result; + +mod cli; +mod commands; + +fn main() -> Result<()> { + let _ = env_logger::try_init()?; + let matches = crate::cli::app().get_matches(); + + match matches.subcommand() { + Some(("profile", matches)) => crate::commands::profile(matches).await, + Some(("gui", _)) => { + unimplemented!() + }, + Some((other, _)) => { + log::error!("No subcommand {} implemented", other); + Ok(()) + }, + + _ => { + log::error!("Don't know what to do"); + Ok(()) + }, + } +} + + diff --git a/gui/Cargo.toml b/gui/Cargo.toml new file mode 100644 index 0000000..d1b7c6d --- /dev/null +++ b/gui/Cargo.toml @@ -0,0 +1,44 @@ +[package] +name = "distrox-gui" +version = "0.1.0" +authors = ["Matthias Beyer "] + +description = "Distributed network build on IPFS, GUI frontend" + +keywords = ["social", "network", "ipfs", "distributed"] +readme = "README.md" +license = "GPL-2.0" + +documentation = "https://docs.rs/distrox" +repository = "https://github.com/matthiasbeyer/distrox" +homepage = "http://github.com/matthiasbeyer/distrox" + +edition = "2018" + +[dependencies] +anyhow = "1" +chrono = { version = "0.4", features = ["serde"] } +cid = "0.5" +clap = "3.0.0-beta.5" +env_logger = "0.8" +futures = "0.3" +log = "0.4" +getset = "0.1" +xdg = "2.4" +tracing = "0.1" +ctrlc = "3.2" + +[dependencies.ipfs] +git = "https://github.com/rs-ipfs/rust-ipfs/" +rev = "ad3ab49b4d9236363969b0f74f14aabc7c906b3b" + +[dependencies.iced] +version = "0.3.0" +features = ["glow", "tokio", "debug"] + +[dependencies.distrox-lib] +path = "../lib" + +[dev-dependencies] +multibase = "0.8" + diff --git a/gui/src/cli.rs b/gui/src/cli.rs new file mode 100644 index 0000000..6ebdfb9 --- /dev/null +++ b/gui/src/cli.rs @@ -0,0 +1,12 @@ +use clap::crate_authors; +use clap::crate_version; +use clap::App; +use clap::Arg; + +pub fn app<'a>() -> App<'a> { + App::new("distrox-gui") + .author(crate_authors!()) + .version(crate_version!()) + .about("Distributed social network, GUI frontend") +} + diff --git a/gui/src/gui/mod.rs b/gui/src/gui/mod.rs new file mode 100644 index 0000000..2eb936e --- /dev/null +++ b/gui/src/gui/mod.rs @@ -0,0 +1,186 @@ +use std::sync::Arc; + +use anyhow::Result; +use iced::Application; +use iced::Column; +use iced::Container; +use iced::Element; +use iced::Length; +use iced::Scrollable; +use iced::TextInput; +use iced::scrollable; +use iced::text_input; + +use distrox_lib::profile::Profile; +use distrox_lib::config::Config; +use distrox_lib::ipfs_client::IpfsClient; + +#[derive(Debug)] +enum Distrox { + Loading, + Loaded(State), + FailedToStart, +} + +#[derive(Debug)] +struct State { + profile: Arc, + + scroll: scrollable::State, + input: text_input::State, + input_value: String, +} + +#[derive(Debug, Clone)] +enum Message { + Loaded(Arc), + FailedToLoad, + + InputChanged(String), + CreatePost, + + PostCreated(cid::Cid), + PostCreationFailed(String), +} + +impl Application for Distrox { + type Executor = iced::executor::Default; // tokio + type Message = Message; + type Flags = (); + + fn new(_flags: ()) -> (Self, iced::Command) { + ( + Distrox::Loading, + iced::Command::perform(async { + match Profile::new_inmemory(Config::default()).await { + Err(_) => Message::FailedToLoad, + Ok(instance) => { + Message::Loaded(Arc::new(instance)) + } + } + }, |m: Message| -> Message { m }) + ) + } + + fn title(&self) -> String { + String::from("distrox") + } + + fn update(&mut self, message: Self::Message, _clipboard: &mut iced::Clipboard) -> iced::Command { + match self { + Distrox::Loading => { + match message { + Message::Loaded(profile) => { + let state = State { + profile, + scroll: scrollable::State::default(), + input: text_input::State::default(), + input_value: String::default(), + }; + *self = Distrox::Loaded(state); + } + + Message::FailedToLoad => { + log::error!("Failed to load"); + *self = Distrox::FailedToStart; + } + + _ => {} + + } + } + + Distrox::Loaded(state) => { + match message { + Message::InputChanged(input) => { + state.input_value = input; + } + + Message::CreatePost => { + if !state.input_value.is_empty() { + let profile = state.profile.clone(); + let input = state.input_value.clone(); + iced::Command::perform(async move { + profile.client().post_text_blob(input).await + }, + |res| match res { + Ok(cid) => Message::PostCreated(cid), + Err(e) => Message::PostCreationFailed(e.to_string()) + }); + } + } + + _ => {} + } + } + + Distrox::FailedToStart => { + unimplemented!() + } + } + iced::Command::none() + } + + fn view(&mut self) -> iced::Element { + match self { + Distrox::Loading => { + let text = iced_native::widget::text::Text::new("Loading"); + + let content = Column::new() + .max_width(800) + .spacing(20) + .push(text); + + Container::new(content) + .width(Length::Fill) + .center_x() + .into() + } + + Distrox::Loaded(state) => { + let input = TextInput::new( + &mut state.input, + "What do you want to tell the world?", + &mut state.input_value, + Message::InputChanged, + ) + .padding(15) + .size(30) + .on_submit(Message::CreatePost); + + let content = Column::new() + .max_width(800) + .spacing(20) + .push(input); + + Scrollable::new(&mut state.scroll) + .padding(40) + .push( + Container::new(content).width(Length::Fill).center_x(), + ) + .into() + } + + Distrox::FailedToStart => { + unimplemented!() + } + } + } + +} + +pub fn run() -> Result<()> { + let settings = iced::Settings { + window: iced::window::Settings { + resizable: true, + decorations: true, + transparent: false, + always_on_top: false, + ..iced::window::Settings::default() + }, + exit_on_close_request: true, + ..iced::Settings::default() + }; + + Distrox::run(settings).map_err(anyhow::Error::from) +} diff --git a/gui/src/main.rs b/gui/src/main.rs new file mode 100644 index 0000000..ea47a93 --- /dev/null +++ b/gui/src/main.rs @@ -0,0 +1,26 @@ +use anyhow::Result; + +mod cli; +mod gui; + +use distrox_lib::*; + +fn main() -> Result<()> { + let _ = env_logger::try_init()?; + let matches = crate::cli::app().get_matches(); + + match matches.subcommand() { + None => crate::gui::run(), + Some((other, _)) => { + log::error!("No subcommand {} implemented", other); + Ok(()) + }, + + _ => { + log::error!("Don't know what to do"); + Ok(()) + }, + } +} + + diff --git a/lib/Cargo.toml b/lib/Cargo.toml new file mode 100644 index 0000000..0655384 --- /dev/null +++ b/lib/Cargo.toml @@ -0,0 +1,47 @@ +[package] +name = "distrox-lib" +version = "0.1.0" +authors = ["Matthias Beyer "] + +description = "Distributed network build on IPFS" + +keywords = ["social", "network", "ipfs", "distributed"] +readme = "README.md" +license = "GPL-2.0" + +documentation = "https://docs.rs/distrox" +repository = "https://github.com/matthiasbeyer/distrox" +homepage = "http://github.com/matthiasbeyer/distrox" + +edition = "2018" + +[dependencies] +anyhow = "1" +async-trait = "0.1" +chrono = { version = "0.4", features = ["serde"] } +cid = "0.5" +clap = "3.0.0-beta.5" +daglib = { git = "https://git.sr.ht/~matthiasbeyer/daglib", branch = "master" } +env_logger = "0.8" +futures = "0.3" +log = "0.4" +tokio = { version = "1", features = ["full", "rt", "macros"] } +mime = "0.3" +rand_core = { version = "0.6", features = ["getrandom"] } +rand_os = "0.2" +ed25519-dalek = "*" +http = "0.2" +serde = "1" +serde_json = "1" +getset = "0.1" +xdg = "2.4" +tracing = "0.1" +ctrlc = "3.2" +libp2p = "0.39.0" + +[dependencies.ipfs] +git = "https://github.com/rs-ipfs/rust-ipfs/" +rev = "ad3ab49b4d9236363969b0f74f14aabc7c906b3b" + +[dev-dependencies] +multibase = "0.8" diff --git a/lib/src/cid.rs b/lib/src/cid.rs new file mode 100644 index 0000000..2957cc5 --- /dev/null +++ b/lib/src/cid.rs @@ -0,0 +1,54 @@ +use anyhow::Result; + +/// Our own CID type +/// +/// Right now the ipfs_api crate does not use a CID type in its interface... hence we would need to +/// convert back-and-forth between String and cid::Cid,... but that's tedious. +/// +/// Hence we just create our own "Cid type" and use that as long as the crate API is stringly +/// typed. +#[derive(Clone, Debug, Eq, PartialEq, Hash, serde::Serialize, serde::Deserialize)] +#[serde(transparent)] +pub struct Cid(String); + +impl AsRef for Cid { + fn as_ref(&self) -> &str { + self.0.as_ref() + } +} + +pub trait TryToCid { + fn try_to_cid(self) -> Result; +} + +impl daglib::NodeId for Cid { +} + +/// Helper function that can be tested +/// +/// Converts a String to a Cid +#[cfg(not(test))] +fn string_to_cid(s: String) -> Result { + string_to_cid_impl(s) +} + +#[cfg(test)] +pub fn string_to_cid(s: String) -> Result { + string_to_cid_impl(s) +} + +fn string_to_cid_impl(s: String) -> Result { + Ok(Cid(s)) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_string_to_cid() { + let s = String::from("QmY2T5EfgLn8qWCt8eus6VX1gJuAp1nmUSdmoehgMxznAf"); + let r = string_to_cid(s); + assert!(r.is_ok(), "Not OK = {:?}", r); + } +} diff --git a/lib/src/client.rs b/lib/src/client.rs new file mode 100644 index 0000000..bb280a5 --- /dev/null +++ b/lib/src/client.rs @@ -0,0 +1,315 @@ +use std::convert::TryFrom; + +use anyhow::Result; +use ipfs::Cid; + +use crate::config::Config; +use crate::ipfs_client::IpfsClient; +use crate::types::Node; +use crate::types::Payload; +use crate::types::DateTime; + +pub struct Client { + pub(crate) ipfs: IpfsClient, + config: Config, +} + +impl std::fmt::Debug for Client { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "Client {{ config: {:?} }}", self.config) + } +} + +impl Client { + pub fn new(ipfs: IpfsClient, config: Config) -> Self { + Client { + ipfs, + config + } + } + + pub async fn exit(self) -> Result<()> { + self.ipfs.exit_daemon().await; + Ok(()) + } + + pub async fn connect(&self, peer: ipfs::MultiaddrWithPeerId) -> Result<()> { + self.ipfs.connect(peer).await + } + + pub async fn post_text_blob(&self, text: String) -> Result { + self.ipfs + .put_dag(text.into()) + .await + .map_err(anyhow::Error::from) + } + + /// Post a text node + /// + /// Pass in the parents if there are any. + /// + /// # Note + /// + /// Does not verify if the `parents` cids point to actual Nodes! + /// + /// # Returns + /// + /// Returns the Cid of the newly created node, or an error + pub async fn post_text_node(&self, parents: Vec, text: String) -> Result { + self.post_text_node_with_datetime(parents, text, now()).await + } + + // For testing + async fn post_text_node_with_datetime(&self, parents: Vec, text: String, datetime: DateTime) -> Result { + let text_blob_cid = self.post_text_blob(text).await?; + + let payload = Payload::new(mime::TEXT_PLAIN_UTF_8.as_ref().to_string(), datetime, text_blob_cid); + let payload_cid = self.post_payload(payload).await?; + + let node = Node::new(crate::consts::protocol_version(), parents, payload_cid); + self.post_node(node).await + } + + async fn post_payload(&self, payload: Payload) -> Result { + self.post(payload).await + } + + async fn post_node(&self, node: Node) -> Result { + self.post(node).await + } + + async fn post>(&self, s: S) -> Result { + self.ipfs.put_dag(s.into()).await.map_err(anyhow::Error::from) + } + + pub async fn get_node(&self, cid: Cid) -> Result { + self.get::(cid).await + } + + pub async fn get_payload(&self, cid: Cid) -> Result { + self.get::(cid).await + } + + async fn get>(&self, cid: Cid) -> Result { + let ipld = self.ipfs + .get_dag(ipfs::IpfsPath::new(ipfs::path::PathRoot::Ipld(cid))) + .await?; + + D::try_from(ipld) + } + + pub async fn get_content_text(&self, cid: Cid) -> Result { + struct S(String); + impl TryFrom for S { + type Error = anyhow::Error; + fn try_from(ipld: ipfs::Ipld) -> Result { + match ipld { + ipfs::Ipld::String(s) => Ok(S(s)), + _ => anyhow::bail!("Not a string"), + } + } + } + + self.get::(cid).await.map(|v| v.0) + } +} + +fn now() -> DateTime { + chrono::offset::Utc::now().into() +} + +#[cfg(test)] +mod tests { + use std::convert::TryFrom; + + use cid::Cid; + + use crate::client::Client; + use crate::config::Config; + use crate::ipfs_client::IpfsClient; + use crate::types::DateTime; + + fn mkdate(y: i32, m: u32, d: u32, hr: u32, min: u32, sec: u32) -> crate::types::DateTime { + use chrono::TimeZone; + + chrono::prelude::Utc.ymd(y, m, d).and_hms(hr, min, sec).into() + } + + async fn mk_ipfs() -> IpfsClient { + let mut opts = ipfs::IpfsOptions::inmemory_with_generated_keys(); + opts.mdns = false; + let (ipfs, fut): (ipfs::Ipfs, _) = ipfs::UninitializedIpfs::new(opts).start().await.unwrap(); + tokio::task::spawn(fut); + ipfs + } + + #[tokio::test] + async fn test_post_text_blob() { + let _ = env_logger::try_init(); + let ipfs = mk_ipfs().await; + let config = Config::default(); + let client = Client::new(ipfs, config); + + let cid = client.post_text_blob(String::from("text")).await; + assert!(cid.is_ok()); + let cid = cid.unwrap(); + let expected_cid = Cid::try_from("bafyreienmqqpz622nxgi7xvcx2jf7p3lyagqkwcj5ieil3mhx2zckfl35u").unwrap(); + assert_eq!(cid, expected_cid, "{} != {}", cid, expected_cid); + } + + #[tokio::test] + async fn test_post_text_node() { + let _ = env_logger::try_init(); + let ipfs = mk_ipfs().await; + let config = Config::default(); + let client = Client::new(ipfs, config); + + let datetime = mkdate(2021, 11, 27, 12, 30, 0); + + let cid = client.post_text_node_with_datetime(Vec::new(), String::from("text"), datetime).await; + assert!(cid.is_ok()); + let cid = cid.unwrap(); + let expected_cid = Cid::try_from("bafyreidem25zq66ktf42l2sjlxmbz5f66bedw3i4ippshhb3h7dxextfty").unwrap(); + assert_eq!(cid, expected_cid, "{} != {}", cid, expected_cid); + } + + #[tokio::test] + async fn test_post_text_node_roundtrip() { + let _ = env_logger::try_init(); + let ipfs = mk_ipfs().await; + let config = Config::default(); + let client = Client::new(ipfs, config); + + let datetime = mkdate(2021, 11, 27, 12, 30, 0); + + let text = "text-roundtrip"; + + let cid = client.post_text_node_with_datetime(Vec::new(), String::from(text), datetime.clone()).await; + assert!(cid.is_ok()); + let cid = cid.unwrap(); + let expected_cid = Cid::try_from("bafyreicwvx755ysg7zfflxhwhl4d6wuuxmmgfexjfvdhgndiugj37bsphq").unwrap(); + assert_eq!(cid, expected_cid, "{} != {}", cid, expected_cid); + + let node = client.get_node(cid).await; + assert!(node.is_ok()); + let node = node.unwrap(); + + assert_eq!(*node.version(), crate::consts::protocol_version()); + assert!(node.parents().is_empty()); + + let payload = client.get_payload(node.payload().clone()).await; + assert!(payload.is_ok()); + let payload = payload.unwrap(); + + assert_eq!(payload.mime(), mime::TEXT_PLAIN_UTF_8.as_ref()); + assert_eq!(payload.timestamp(), &datetime); + + let content = client.get_content_text(payload.content().clone()).await; + assert!(content.is_ok(), "not ok: {:?}", content); + let content = content.unwrap(); + + assert_eq!(content, text); + } + + #[tokio::test] + async fn test_post_text_chain() { + let _ = env_logger::try_init(); + let ipfs = mk_ipfs().await; + let config = Config::default(); + let client = Client::new(ipfs, config); + + let chain_elements = vec![ + (mkdate(2021, 11, 27, 12, 30, 0), "text1", "bafyreidaxkxog3bssyxxjxlsubgg6wauxbobp7gwyucs6gwzyrtsavb7yu"), + (mkdate(2021, 11, 27, 12, 31, 0), "text2", "bafyreifsgfl6tvcdn42kihjryg7fpjyjgi4v56bud2m2yniqjrrfn3ils4"), + (mkdate(2021, 11, 27, 12, 32, 0), "text3", "bafyreifnim44y6zfsc7jrf4xs3lbawlc4qqmk4tgmbqnflbggmvvuvul7a"), + ]; + + let mut prev: Option = None; + for (datetime, text, expected_cid) in chain_elements { + let parents = if let Some(previous) = prev.as_ref() { + vec![previous.clone()] + } else { + Vec::new() + }; + + let cid = client.post_text_node_with_datetime(parents, String::from(text), datetime.clone()).await; + assert!(cid.is_ok()); + let cid = cid.unwrap(); + let expected_cid = Cid::try_from(expected_cid).unwrap(); + assert_eq!(cid, expected_cid, "{} != {}", cid, expected_cid); + prev = Some(cid); + } + } + + #[tokio::test] + async fn test_post_text_dag() { + let _ = env_logger::try_init(); + let ipfs = mk_ipfs().await; + let config = Config::default(); + let client = Client::new(ipfs, config); + + async fn post_chain(client: &Client, chain_elements: &Vec<(DateTime, &str, &str)>) { + let mut prev: Option = None; + for (datetime, text, expected_cid) in chain_elements { + let parents = if let Some(previous) = prev.as_ref() { + vec![previous.clone()] + } else { + Vec::new() + }; + + let cid = client.post_text_node_with_datetime(parents, String::from(*text), datetime.clone()).await; + assert!(cid.is_ok()); + let cid = cid.unwrap(); + let expected_cid = Cid::try_from(*expected_cid).unwrap(); + assert_eq!(cid, expected_cid, "{} != {}", cid, expected_cid); + prev = Some(cid); + } + } + + // The following posts a DAG like this: + // + // * -- * -- * _ + // \ + // * -- * -- * -- * + // / + // * - + + let chain_1_elements = vec![ + (mkdate(2021, 11, 27, 12, 30, 0), "text1", "bafyreidaxkxog3bssyxxjxlsubgg6wauxbobp7gwyucs6gwzyrtsavb7yu"), + (mkdate(2021, 11, 27, 12, 31, 0), "text2", "bafyreifsgfl6tvcdn42kihjryg7fpjyjgi4v56bud2m2yniqjrrfn3ils4"), + (mkdate(2021, 11, 27, 12, 32, 0), "text3", "bafyreifnim44y6zfsc7jrf4xs3lbawlc4qqmk4tgmbqnflbggmvvuvul7a"), + ]; + + let chain_2_elements = vec![ + (mkdate(2021, 11, 27, 12, 32, 0), "text4", "bafyreibfkbslobjydkl3tuiqms7dk243fendyqxi5myqkhxquz7arayuwe"), + (mkdate(2021, 11, 27, 12, 32, 0), "text5", "bafyreicpzj4lfhzsx5pacp2otk7qyyx353lwsvmkp4aplwgvyisg3y4mjm"), + ]; + + post_chain(&client, &chain_1_elements).await; + post_chain(&client, &chain_2_elements).await; + + let cid = client.post_text_node_with_datetime(Vec::new(), String::from("text6"), mkdate(2021, 11, 27, 12, 32, 0)).await; + assert!(cid.is_ok()); + let cid = cid.unwrap(); + let expected_cid = Cid::try_from("bafyreifcpqvxzrgmcbdx5omysjfyupsvjxlrfzww5yh75ld7f7ox3vzno4").unwrap(); + assert_eq!(cid, expected_cid, "{} != {}", cid, expected_cid); + + let parents = vec![ + // latest node in chain_1_elements + ipfs::Cid::try_from("bafyreifnim44y6zfsc7jrf4xs3lbawlc4qqmk4tgmbqnflbggmvvuvul7a").unwrap(), + + // latest node in chain_2_elements + ipfs::Cid::try_from("bafyreicpzj4lfhzsx5pacp2otk7qyyx353lwsvmkp4aplwgvyisg3y4mjm").unwrap(), + + // single node "text6" + cid + ]; + + let cid = client.post_text_node_with_datetime(parents, String::from("text7"), mkdate(2021, 11, 27, 12, 32, 0)).await; + assert!(cid.is_ok()); + let cid = cid.unwrap(); + let expected_cid = Cid::try_from("bafyreieuac7kvefkiu5ls7tqumaef5qiur7l3moa33ay2kaxxpjmfdjbey").unwrap(); + assert_eq!(cid, expected_cid, "{} != {}", cid, expected_cid); + } + +} diff --git a/lib/src/config.rs b/lib/src/config.rs new file mode 100644 index 0000000..0d25e78 --- /dev/null +++ b/lib/src/config.rs @@ -0,0 +1,10 @@ +#[derive(Debug)] +pub struct Config { +} + +impl Default for Config { + fn default() -> Self { + Config { } + } +} + diff --git a/lib/src/consts.rs b/lib/src/consts.rs new file mode 100644 index 0000000..79ff2f5 --- /dev/null +++ b/lib/src/consts.rs @@ -0,0 +1,4 @@ +pub fn protocol_version() -> String { + String::from("1") +} + diff --git a/lib/src/ipfs_client.rs b/lib/src/ipfs_client.rs new file mode 100644 index 0000000..964b22c --- /dev/null +++ b/lib/src/ipfs_client.rs @@ -0,0 +1,5 @@ +#[cfg(not(test))] +pub type IpfsClient = ipfs::Ipfs; + +#[cfg(test)] +pub type IpfsClient = ipfs::Ipfs; diff --git a/lib/src/lib.rs b/lib/src/lib.rs new file mode 100644 index 0000000..759ea14 --- /dev/null +++ b/lib/src/lib.rs @@ -0,0 +1,6 @@ +pub mod client; +pub mod config; +pub mod consts; +pub mod ipfs_client; +pub mod profile; +pub mod types; diff --git a/lib/src/profile/mod.rs b/lib/src/profile/mod.rs new file mode 100644 index 0000000..e3ce175 --- /dev/null +++ b/lib/src/profile/mod.rs @@ -0,0 +1,182 @@ +use std::path::PathBuf; +use std::convert::TryInto; + +use anyhow::Context; +use anyhow::Result; + +use crate::client::Client; +use crate::config::Config; +use crate::ipfs_client::IpfsClient; + +mod state; +use state::*; + +#[derive(Debug, getset::Getters)] +pub struct Profile { + state: ProfileState, + + #[getset(get = "pub")] + client: Client, +} + +impl Profile { + pub async fn create(state_dir: &StateDir, name: &str, config: Config) -> Result { + 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, config, name.to_string(), keypair).await + } + + #[cfg(test)] + async fn new_inmemory(config: Config, name: &str) -> Result { + 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, config, format!("inmemory-{}", name), keypair).await + } + + async fn new(ipfs: IpfsClient, config: Config, profile_name: String, keypair: libp2p::identity::Keypair) -> Result { + let client = Client::new(ipfs, config); + 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 connect(&self, peer: ipfs::MultiaddrWithPeerId) -> Result<()> { + self.client.connect(peer).await + } + + async fn ipfs_path(state_dir: &StateDir) -> Result { + 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 { + 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 { + 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(config: Config, name: &str) -> Result { + 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, config), + }) + } + + pub async fn exit(self) -> Result<()> { + self.client.exit().await + } + +} + + +#[cfg(test)] +mod tests { + use super::*; + use std::convert::TryFrom; + use crate::config::Config; + + #[tokio::test] + async fn test_create_profile() { + let _ = env_logger::try_init(); + let profile = Profile::new_inmemory(Config::default(), "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(Config::default(), "test-create-profile-and-helloworld").await; + assert!(profile.is_ok()); + let profile = profile.unwrap(); + assert!(profile.head().is_none()); + } + +} diff --git a/lib/src/profile/state.rs b/lib/src/profile/state.rs new file mode 100644 index 0000000..4075b52 --- /dev/null +++ b/lib/src/profile/state.rs @@ -0,0 +1,129 @@ +use std::path::PathBuf; +use std::convert::TryFrom; +use std::convert::TryInto; + +use anyhow::Context; +use anyhow::Result; +use tokio::io::AsyncWriteExt; + +#[derive(Debug)] +pub struct StateDir(PathBuf); + +impl StateDir { + pub fn ipfs(&self) -> PathBuf { + self.0.join("ipfs") + } + + pub fn profile_state(&self) -> PathBuf { + self.0.join("profile_state") + } + + pub fn display(&self) -> std::path::Display { + self.0.display() + } +} + +impl From for StateDir { + fn from(p: PathBuf) -> Self { + Self(p) + } +} + +#[derive(getset::Getters)] +pub struct ProfileState { + #[getset(get = "pub")] + profile_head: Option, + + #[getset(get = "pub")] + profile_name: String, + + #[getset(get = "pub")] + keypair: libp2p::identity::Keypair, +} + +impl ProfileState { + pub(super) fn new(profile_name: String, keypair: libp2p::identity::Keypair) -> Self { + Self { + profile_head: None, + profile_name, + keypair + } + } +} + +impl std::fmt::Debug for ProfileState { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "ProfileState {{ name = {}, head = {:?} }}", self.profile_name, self.profile_head) + } +} + +#[derive(Debug, serde::Serialize, serde::Deserialize, getset::Getters)] +pub(super) struct ProfileStateSaveable { + profile_head: Option>, + profile_name: String, + keypair: Vec, +} + +impl ProfileStateSaveable { + pub(super) fn new(s: &ProfileState) -> Result { + Ok(Self { + profile_head: s.profile_head.clone().map(|v| v.to_bytes()), + profile_name: s.profile_name.clone(), + keypair: match s.keypair { + libp2p::identity::Keypair::Ed25519(ref kp) => Vec::from(kp.encode()), + _ => anyhow::bail!("Only keypair type ed25519 supported"), + } + }) + } + + pub async fn save_to_disk(&self, state_dir_path: &StateDir) -> Result<()> { + let state_s = serde_json::to_string(&self).context("Serializing state")?; + tokio::fs::OpenOptions::new() + .create_new(false) // do not _always_ create a new file + .create(true) + .truncate(true) + .write(true) + .open(&state_dir_path.profile_state()) + .await + .with_context(|| format!("Opening {}", state_dir_path.profile_state().display()))? + .write_all(state_s.as_bytes()) + .await + .map(|_| ()) + .with_context(|| format!("Writing to {}", state_dir_path.profile_state().display())) + .map_err(anyhow::Error::from) + } + + pub async fn load_from_disk(state_dir_path: &StateDir) -> Result { + log::trace!("Loading from disk: {:?}", state_dir_path.profile_state().display()); + let reader = tokio::fs::OpenOptions::new() + .read(true) + .open(&state_dir_path.profile_state()) + .await + .context("Opening state file")? + .into_std() + .await; + + log::trace!("Parsing state file"); + serde_json::from_reader(reader) + .context("Parsing state file") + .map_err(anyhow::Error::from) + } + +} + +impl TryInto for ProfileStateSaveable { + type Error = anyhow::Error; + + fn try_into(mut self) -> Result { + Ok(ProfileState { + profile_head: self.profile_head.map(|h| cid::Cid::try_from(h)).transpose()?, + profile_name: self.profile_name, + keypair: { + let kp = libp2p::identity::ed25519::Keypair::decode(&mut self.keypair)?; + libp2p::identity::Keypair::Ed25519(kp) + }, + }) + } +} + + diff --git a/lib/src/types/datetime.rs b/lib/src/types/datetime.rs new file mode 100644 index 0000000..00d739a --- /dev/null +++ b/lib/src/types/datetime.rs @@ -0,0 +1,35 @@ +use std::convert::TryFrom; +use anyho