summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.builds/debian.yml13
-rw-r--r--.github/workflows/ci.yml70
-rw-r--r--.gitignore1
-rw-r--r--Cargo.toml51
-rw-r--r--README.md37
-rw-r--r--assets/index.html11
-rw-r--r--assets/index_post.html0
-rw-r--r--assets/index_pre.html3
-rw-r--r--assets/style.css25
-rw-r--r--cli/Cargo.toml50
-rw-r--r--cli/src/cli.rs119
-rw-r--r--cli/src/main.rs28
-rw-r--r--cli/src/profile.rs224
-rw-r--r--distrox.toml46
-rw-r--r--gui/Cargo.toml55
-rw-r--r--gui/src/app/handler.rs162
-rw-r--r--gui/src/app/message.rs72
-rw-r--r--gui/src/app/mod.rs259
-rw-r--r--gui/src/cli.rs19
-rw-r--r--gui/src/gossip.rs49
-rw-r--r--gui/src/main.rs25
-rw-r--r--gui/src/post.rs47
-rw-r--r--gui/src/timeline.rs103
-rw-r--r--lib/Cargo.toml63
-rw-r--r--lib/src/cid.rs54
-rw-r--r--lib/src/client.rs339
-rw-r--r--lib/src/consts.rs4
-rw-r--r--lib/src/gossip/deserializer.rs57
-rw-r--r--lib/src/gossip/mod.rs6
-rw-r--r--lib/src/gossip/msg.rs17
-rw-r--r--lib/src/ipfs_client.rs5
-rw-r--r--lib/src/lib.rs7
-rw-r--r--lib/src/profile/device.rs31
-rw-r--r--lib/src/profile/mod.rs226
-rw-r--r--lib/src/profile/state.rs160
-rw-r--r--lib/src/stream.rs46
-rw-r--r--lib/src/types/datetime.rs41
-rw-r--r--lib/src/types/mod.rs8
-rw-r--r--lib/src/types/node.rs88
-rw-r--r--lib/src/types/payload.rs72
-rw-r--r--shell.nix56
-rw-r--r--src/cli.rs43
-rw-r--r--src/configuration.rs107
-rw-r--r--src/gui.rs31
-rw-r--r--src/main.rs115
-rw-r--r--src/middleware.rs108
-rw-r--r--src/model.rs146
-rw-r--r--src/server.rs78
-rw-r--r--src/types/block.rs85
-rw-r--r--src/types/content.rs104
-rw-r--r--src/types/mod.rs5
-rw-r--r--src/types/payload.rs279
-rw-r--r--src/types/util.rs172
-rw-r--r--src/version.rs4
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
diff --git a/.gitignore b/.gitignore
index eccd7b4..6aa1064 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,3 @@
/target/
**/*.rs.bk
+Cargo.lock
diff --git a/Cargo.toml b/Cargo.toml
index a53ead9..f0e2060 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -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",
+]
diff --git a/README.md b/README.md
index ffa8191..4f18157 100644
--- a/README.md
+++ b/README.md
@@ -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