diff options
54 files changed, 2568 insertions, 1458 deletions
diff --git a/.builds/debian.yml b/.builds/debian.yml deleted file mode 100644 index 5d57dd8..0000000 --- a/.builds/debian.yml +++ /dev/null @@ -1,13 +0,0 @@ -image: debian/stable -packages: - - curl - - openssl - - libssl-dev - - pkg-config -sources: - - https://git.sr.ht/~matthiasbeyer/distrox -tasks: - - install: curl https://sh.rustup.rs -sSf | sh -s -- -y - - check: | - cd distrox - PATH="$HOME/.cargo/bin:$PATH" cargo check --all --all-features diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..90fb550 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,70 @@ +on: [push, pull_request] + +name: CI + +jobs: + check: + name: Check + runs-on: ubuntu-latest + strategy: + matrix: + rust: + - 1.57.0 + - stable + - beta + + steps: + - name: Install dependencies + run: sudo apt-get install -y openssl pkg-config zlib1g protobuf-compiler + + - name: Checkout sources + uses: actions/checkout@v2 + + - name: Install toolchain + uses: actions-rs/toolchain@v1 + with: + toolchain: ${{ matrix.rust }} + override: true + + - name: Run cargo check + uses: actions-rs/cargo@v1 + with: + command: check + + test: + needs: [check] + name: Test Suite + runs-on: ubuntu-latest + strategy: + matrix: + rust: + - 1.57.0 + - stable + - beta + steps: + - name: Install dependencies + run: sudo apt-get install -y openssl pkg-config zlib1g protobuf-compiler + + - name: Checkout sources + uses: actions/checkout@v2 + + - name: Install toolchain + uses: actions-rs/toolchain@v1 + with: + toolchain: ${{ matrix.rust }} + override: true + + - name: Cache Setup + uses: actions/cache@v2 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + target + key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} + + - name: Run cargo test + uses: actions-rs/cargo@v1 + with: + command: test + args: --all --all-features @@ -1,2 +1,3 @@ /target/ **/*.rs.bk +Cargo.lock @@ -1,45 +1,6 @@ -[package] -name = "distrox" -version = "0.1.0" -authors = ["Matthias Beyer <mail@beyermatthias.de>"] - -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] -is-match = "0.1" -anyhow = "1" -futures = "0.3" -ipfs-api = { version = "0.7", features = ["actix"], default-features = false } -serde = "1" -serde_derive = "1" -serde_json = "1" -mime = "0.3" -chrono = { version = "0.4", features = ["serde"] } -uuid = { version = "0.7", features = ["serde", "v4"] } -clap = "2" -log = "0.4" -env_logger = "0.7" -config = "0.9" -toml = "0.4" -hyper = "0.12" -itertools = "0.7" -tokio = { version = "0.2", features = ["full"] } -add_getters_setters = "1.1" -xdg = "2" -structopt = "0.3" -web-view = "0.6" -handlebars = "3" -actix-rt = "1" -actix-web = "2" -failure = "0.1" -pidlock = { git = "https://github.com/matthiasbeyer/pidlock", branch = "my-master" } +[workspace] +members = [ + "cli", + "gui", + "lib", +] @@ -1,25 +1,22 @@ # distrox -A distributed social network build on IPFS. - -## TODO - -1. Implement protocol -1. Implement working prototype with CLI frontend -1. Implement GUI frontend with azul or Qt. -1. Publish POC on IPFS forums, Rust forums, reddit, hackernews, etc and ask for - comments and contributors. - - -## POC/MVP should contain - -1. Publishing content (text or media data) -1. Following profiles - 1. requires investigation how to do right, because that information should - be stored locally, but also should be synced to other devices of the - profile. -1. Merging other devices - 1. Merging strategies must be defined and investigated +A distributed social network build on IPFS/IPLD. + +## Roadmap + +This is the roadmap from the current state to a MVP/POC. +These "features" are not necessarily ordered, but may depend on another. + +* Gossipping via gossipsub instead of pubsub +* Profile discovery via gossipsub +* Profile caching +* Profile following +* Multi-device support via raft concensus over gossipsub +* (CLI) cache node functionality ("multi device" node that only caches posts of + a profile) +* (GUI) Timeline polishing +* (GUI) Markdown support in Text posts +* (GUI) Non-Text posts: Directories (MVP for image/video/audio posts) ## License diff --git a/assets/index.html b/assets/index.html deleted file mode 100644 index eb9e2e0..0000000 --- a/assets/index.html +++ /dev/null @@ -1,11 +0,0 @@ - </style> - </head> - <body> - <div id="sidebar_left"> - </div> - <div id="content"> - </div> - <div id="sidebar_right"> - </div> - </body> -</html> diff --git a/assets/index_post.html b/assets/index_post.html deleted file mode 100644 index e69de29..0000000 --- a/assets/index_post.html +++ /dev/null diff --git a/assets/index_pre.html b/assets/index_pre.html deleted file mode 100644 index da6f618..0000000 --- a/assets/index_pre.html +++ /dev/null @@ -1,3 +0,0 @@ -<html> - <head> - <style> diff --git a/assets/style.css b/assets/style.css deleted file mode 100644 index 120b1bd..0000000 --- a/assets/style.css +++ /dev/null @@ -1,25 +0,0 @@ -body { - background-color: black; -} - -#sidebar_left { - background-color: grey; - height: 100%; - position: fixed; - top: 0; - left: 0; - width: 10%; - min-width: 15em; - float: left; -} - -#sidebar_right { - background-color: grey; - height: 100%; - position: fixed; - top: 0; - right: 0; - width: 10%; - min-width: 15em; - float: right; -} diff --git a/cli/Cargo.toml b/cli/Cargo.toml new file mode 100644 index 0000000..215efe1 --- /dev/null +++ b/cli/Cargo.toml @@ -0,0 +1,50 @@ +[package] +name = "distrox-cli" +version = "0.1.0" +authors = ["Matthias Beyer <mail@beyermatthias.de>"] + +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" +editor-input = "0.1.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..4b52ad6 --- /dev/null +++ b/cli/src/cli.rs @@ -0,0 +1,119 @@ +use clap::crate_authors; +use clap::crate_version; +use clap::App; +use clap::Arg; +use clap::ArgGroup; + +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) + .multiple(true) + .value_name("MULTIADDR") + .about("Connect to MULTIADDR as well") + ) + + .arg(Arg::new("listen") + .long("listen") + .required(false) + .takes_value(true) + .multiple(true) + .value_name("MULTIADDR") + .about("Listen on MULTIADDR, e.g. '/ip4/127.0.0.1/tcp/10000'") + ) + ) + + .subcommand(App::new("cat") + .author(crate_authors!()) + .version(crate_version!()) + .about("Read complete timeline of profile") + + .arg(Arg::new("name") + .long("name") + .required(true) + .takes_value(true) + .value_name("NAME") + .about("Name of the profile") + ) + ) + + .subcommand(App::new("post") + .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 to post to") + ) + + .arg(Arg::new("editor") + .long("editor") + .short('e') + .required(false) + .takes_value(false) + .about("Launch the editor for the text to be posted") + .conflicts_with("text") + ) + .arg(Arg::new("text") + .long("text") + .required(true) + .takes_value(true) + .value_name("TEXT") + .about("The text to be posted") + .conflicts_with("editor") + ) + .group(ArgGroup::new("text-or-editor") + .args(&["text", "editor"]) + .required(true) // one must be present + ) + ) + ) + + .subcommand(App::new("gui") + .author(crate_authors!()) + .version(crate_version!()) + .about("Start the distrox gui") + ) +} diff --git a/cli/src/main.rs b/cli/src/main.rs new file mode 100644 index 0000000..c4050ac --- /dev/null +++ b/cli/src/main.rs @@ -0,0 +1,28 @@ +use anyhow::Result; + +mod cli; +mod profile; + +#[tokio::main] +async fn main() -> Result<()> { + let _ = env_logger::try_init()?; + let matches = crate::cli::app().get_matches(); + + match matches.subcommand() { + Some(("profile", matches)) => crate::profile::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/cli/src/profile.rs b/cli/src/profile.rs new file mode 100644 index 0000000..d751116 --- /dev/null +++ b/cli/src/profile.rs @@ -0,0 +1,224 @@ +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::profile::Profile; +use distrox_lib::types::Payload; + +pub async fn profile(matches: &ArgMatches) -> Result<()> { + match matches.subcommand() { + Some(("create", m)) => profile_create(m).await, + Some(("serve", m)) => profile_serve(m).await, + Some(("post", m)) => profile_post(m).await, + Some(("cat", m)) => profile_cat(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).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 listen_addrs = matches.values_of("listen") + .map(|v| { + v.map(|s| s.parse::<ipfs::Multiaddr>().map_err(anyhow::Error::from)) + .collect::<Result<Vec<_>>>() + }) + .transpose()?; + let connect_peer = matches.values_of("connect") + .map(|v| { + v.map(|s| { + s.parse::<MultiaddrWithPeerId>().map_err(anyhow::Error::from) + }) + .collect::<Result<Vec<_>>>() + }) + .transpose()?; + + let state_dir = Profile::state_dir_path(&name)?; + + log::info!("Loading '{}' from {}", name, state_dir.display()); + let profile = Profile::load(&name).await?; + log::info!("Profile loaded"); + if let Some(head) = profile.head().as_ref() { + log::info!("Profile HEAD = {}", head); + } + + if let Some(listen) = listen_addrs { + for l in listen { + log::debug!("Adding listening address: {}", l); + profile.listen_on(l).await?; + } + } + + { + let addrs = profile.client().own_addresses().await?; + if addrs.is_empty() { + log::error!("No own address"); + } else { + for addr in addrs { + log::info!("Own addr: {}", addr); + } + } + } + + if let Some(connect_to) = connect_peer { + for c in connect_to { + log::info!("Connecting to {:?}", c); + profile.connect(c).await?; + } + } + + let mut gossip_channel = Box::pin({ + profile.client() + .pubsub_subscribe("distrox".to_string()) + .await + .map(|stream| { + use distrox_lib::gossip::GossipDeserializer; + use distrox_lib::gossip::LogStrategy; + + GossipDeserializer::<LogStrategy>::new().run(stream) + })? + }); + + let running = Arc::new(AtomicBool::new(true)); + let r = running.clone(); + + let own_peer_id = profile.client().own_id().await?; + + ctrlc::set_handler(move || { + r.store(false, Ordering::SeqCst); + }).context("Error setting Ctrl-C handler")?; + + log::info!("Serving..."); + while running.load(Ordering::SeqCst) { + use futures::stream::StreamExt; + use distrox_lib::gossip::GossipMessage; + + tokio::time::sleep(std::time::Duration::from_millis(500)).await; // sleep not so busy + + tokio::select! { + own = profile.gossip_own_state("distrox".to_string()) => own?, + other = gossip_channel.next() => { + let gossip_myself = other.as_ref().map(|(source, _)| *source == own_peer_id).unwrap_or(false); + + if !gossip_myself { + log::trace!("Received gossip: {:?}", other); + } + } + } + } + log::info!("Shutting down..."); + profile.exit().await +} + +async fn profile_post(matches: &ArgMatches) -> Result<()> { + let text = match matches.value_of("text") { + Some(text) => String::from(text), + None => if matches.is_present("editor") { + editor_input::input_from_editor("")? + } else { + unreachable!() + } + }; + + 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()); + + log::info!("Loading '{}' from {}", name, state_dir.display()); + let mut profile = Profile::load(&name).await?; + log::info!("Profile loaded"); + log::info!("Profile HEAD = {:?}", profile.head()); + + log::info!("Posting text..."); + profile.post_text(text).await?; + log::info!("Posting text finished"); + profile.save().await?; + log::info!("Saving profile state to disk finished"); + profile.exit().await +} + +async fn profile_cat(matches: &ArgMatches) -> Result<()> { + use distrox_lib::stream::NodeStreamBuilder; + use futures::stream::StreamExt; + + 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()); + + log::info!("Loading '{}' from {}", name, state_dir.display()); + let profile = Profile::load(&name).await?; + log::info!("Profile loaded"); + if let Some(head) = profile.head() { + log::info!("Profile HEAD = {:?}", head); + NodeStreamBuilder::starting_from(head.clone()) + .into_stream(profile.client().clone()) + .then(|node| async { + match node { + Err(e) => Err(e), + Ok(node) => { + profile.client() + .get_payload(node.payload()) + .await + } + } + }) + .then(|payload| async { + match payload { + Err(e) => Err(e), + Ok(payload) => { + profile.client() + .get_content_text(payload.content()) + .await + .map(|text| (payload, text)) + } + } + }) + .then(|res| async { + use std::io::Write; + match res { + Err(e) => { + let out = std::io::stderr(); + let mut lock = out.lock(); + writeln!(lock, "Error: {:?}", e)?; + } + Ok((payload, text)) => { + let out = std::io::stdout(); + let mut lock = out.lock(); + writeln!(lock, "{time} - {cid}", + time = payload.timestamp().inner(), + cid = payload.content())?; + + writeln!(lock, "{text}", text = text)?; + writeln!(lock, "")?; + }, + } + Ok(()) + }) + .collect::<Vec<Result<()>>>() + .await + .into_iter() + .collect::<Result<()>>()?; + } else { + eprintln!("Profile has no posts"); + } + + Ok(()) +} diff --git a/distrox.toml b/distrox.toml deleted file mode 100644 index f150886..0000000 --- a/distrox.toml +++ /dev/null @@ -1,46 +0,0 @@ -# The API URL -ipfs-api-url = "127.0.0.1" - -# The API Port -ipfs-api-port = 5001 - -# The Port for the app itself -app-port = 5002 - -# Whether to automatically "ipfs pin" chain objects -autoserve-chains = true - -# Whether to automatically "ipfs pin" foreign posts if their content is text -autoserve-text-posts = true - -# Whether to serve content/chains from blocked profiles -serve-blocked = false - -# Whether to automatically "ipfs pin" followed profiles -autoserve-followed = true - -# Default amount of bytes which are loaded for each post -max-autoload-per-post = 1024 - -# List of Mimetypes which should not be served -autoserve-blacklist = [] - -# List of Mimetypes which can be served -autoserve-whitelist = [] - -# Name under which to provide the local device. E.G. -# Some("/ipfs/QmVrLsEDn27sScp3k23sgZNefVTjSAL3wpgW1iWPi4MgoY") -# -# If none, one will be generated and set -# device_name = "" - -# Key to sign stuff that comes from this device. -# -# Create by using `ipfs key gen <name>` -#device_key = "" - -# Devices for the profile -# E.G: -# ["/ipfs/QmVrLsEDn27sScp3k23sgZNefVTjSAL3wpgW1iWPi4MgoY"] -devices = [] - diff --git a/gui/Cargo.toml b/gui/Cargo.toml new file mode 100644 index 0000000..30b6ac6 --- /dev/null |