add part 4 draft tutorial
This commit is contained in:
81
part_4_posts_streams/src/db.rs
Normal file
81
part_4_posts_streams/src/db.rs
Normal file
@ -0,0 +1,81 @@
|
||||
use std::path::Path;
|
||||
|
||||
use log::{debug, info};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sled::{Db, IVec, Result, Tree};
|
||||
|
||||
/// Scuttlebutt peer data.
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub struct Peer {
|
||||
pub public_key: String,
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
impl Peer {
|
||||
/// Create a new instance of the Peer struct using the given public
|
||||
/// key. A default value is set for name.
|
||||
pub fn new(public_key: &str) -> Peer {
|
||||
Peer {
|
||||
public_key: public_key.to_string(),
|
||||
name: "".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Modify the name field of an instance of the Peer struct, leaving
|
||||
/// the other values unchanged.
|
||||
pub fn set_name(self, name: &str) -> Peer {
|
||||
Self {
|
||||
name: name.to_string(),
|
||||
..self
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// An instance of the key-value database and relevant trees.
|
||||
#[allow(dead_code)]
|
||||
#[derive(Clone)]
|
||||
pub struct Database {
|
||||
/// The sled database instance.
|
||||
db: Db,
|
||||
/// A database tree containing Peer struct instances for all the peers
|
||||
/// we are subscribed to.
|
||||
peer_tree: Tree,
|
||||
}
|
||||
|
||||
impl Database {
|
||||
/// Initialise the database by opening the database file, loading the
|
||||
/// peers tree and returning an instantiated Database struct.
|
||||
pub fn init(path: &Path) -> Self {
|
||||
// Open the database at the given path.
|
||||
// The database will be created if it does not yet exist.
|
||||
// This code will panic if an IO error is encountered.
|
||||
info!("Initialising sled database");
|
||||
let db = sled::open(path).expect("Failed to open database");
|
||||
debug!("Opening 'peers' database tree");
|
||||
let peer_tree = db
|
||||
.open_tree("peers")
|
||||
.expect("Failed to open 'peers' database tree");
|
||||
|
||||
Database { db, peer_tree }
|
||||
}
|
||||
|
||||
/// Add a peer to the database by inserting the public key into the peer
|
||||
/// tree.
|
||||
pub fn add_peer(&self, peer: Peer) -> Result<Option<IVec>> {
|
||||
debug!("Serializing peer data for {} to bincode", &peer.public_key);
|
||||
let peer_bytes = bincode::serialize(&peer).unwrap();
|
||||
|
||||
debug!(
|
||||
"Inserting peer {} into 'peers' database tree",
|
||||
&peer.public_key
|
||||
);
|
||||
self.peer_tree.insert(&peer.public_key, peer_bytes)
|
||||
}
|
||||
|
||||
/// Remove a peer from the database, as represented by the given public
|
||||
/// key.
|
||||
pub fn remove_peer(&self, public_key: &str) -> Result<()> {
|
||||
debug!("Removing peer {} from 'peers' database tree", &public_key);
|
||||
self.peer_tree.remove(&public_key).map(|_| ())
|
||||
}
|
||||
}
|
25
part_4_posts_streams/src/main.rs
Normal file
25
part_4_posts_streams/src/main.rs
Normal file
@ -0,0 +1,25 @@
|
||||
mod db;
|
||||
mod routes;
|
||||
mod sbot;
|
||||
mod utils;
|
||||
|
||||
use rocket::{launch, routes};
|
||||
use rocket_dyn_templates::Template;
|
||||
use xdg::BaseDirectories;
|
||||
|
||||
use crate::{db::Database, routes::*};
|
||||
|
||||
#[launch]
|
||||
async fn rocket() -> _ {
|
||||
// Create the key-value database.
|
||||
let xdg_dirs = BaseDirectories::with_prefix("lykin").unwrap();
|
||||
let db_path = xdg_dirs
|
||||
.place_config_file("database")
|
||||
.expect("cannot create database directory");
|
||||
let db = Database::init(&db_path);
|
||||
|
||||
rocket::build()
|
||||
.manage(db)
|
||||
.attach(Template::fairing())
|
||||
.mount("/", routes![home, subscribe_form, unsubscribe_form])
|
||||
}
|
141
part_4_posts_streams/src/routes.rs
Normal file
141
part_4_posts_streams/src/routes.rs
Normal file
@ -0,0 +1,141 @@
|
||||
use log::{info, warn};
|
||||
use rocket::{
|
||||
form::Form,
|
||||
get, post,
|
||||
request::FlashMessage,
|
||||
response::{Flash, Redirect},
|
||||
uri, FromForm, State,
|
||||
};
|
||||
use rocket_dyn_templates::{context, Template};
|
||||
|
||||
use crate::{
|
||||
db::{Database, Peer},
|
||||
sbot, utils,
|
||||
};
|
||||
|
||||
#[derive(FromForm)]
|
||||
pub struct PeerForm {
|
||||
pub public_key: String,
|
||||
}
|
||||
|
||||
#[get("/")]
|
||||
pub async fn home(flash: Option<FlashMessage<'_>>) -> Template {
|
||||
let whoami = match sbot::whoami().await {
|
||||
Ok(id) => id,
|
||||
Err(e) => format!("Error making `whoami` RPC call: {}. Please ensure the local go-sbot is running and refresh.", e),
|
||||
};
|
||||
|
||||
Template::render("base", context! { whoami: whoami, flash: flash })
|
||||
}
|
||||
|
||||
#[post("/subscribe", data = "<peer>")]
|
||||
pub async fn subscribe_form(
|
||||
db: &State<Database>,
|
||||
peer: Form<PeerForm>,
|
||||
) -> Result<Redirect, Flash<Redirect>> {
|
||||
if let Err(e) = utils::validate_public_key(&peer.public_key) {
|
||||
let validation_err_msg = format!("Public key {} is invalid: {}", &peer.public_key, e);
|
||||
warn!("{}", validation_err_msg);
|
||||
return Err(Flash::error(Redirect::to(uri!(home)), validation_err_msg));
|
||||
} else {
|
||||
info!("Public key {} is valid", &peer.public_key);
|
||||
// Retrieve the name of the peer to which we are subscribing.
|
||||
let peer_name = match sbot::get_name(&peer.public_key).await {
|
||||
Ok(name) => name,
|
||||
Err(e) => {
|
||||
warn!("Failed to fetch name for peer {}: {}", &peer.public_key, e);
|
||||
// Return an empty string if an error occurs.
|
||||
String::from("")
|
||||
}
|
||||
};
|
||||
let peer_info = Peer::new(&peer.public_key).set_name(&peer_name);
|
||||
|
||||
match sbot::follow_if_not_following(&peer.public_key).await {
|
||||
Ok(_) => {
|
||||
// Add the peer to the database.
|
||||
if db.add_peer(peer_info).is_ok() {
|
||||
info!("Added {} to 'peers' database tree", &peer.public_key);
|
||||
} else {
|
||||
let err_msg = format!(
|
||||
"Failed to add peer {} to 'peers' database tree",
|
||||
&peer.public_key
|
||||
);
|
||||
warn!("{}", err_msg);
|
||||
return Err(Flash::error(Redirect::to(uri!(home)), err_msg));
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("{}", e);
|
||||
return Err(Flash::error(Redirect::to(uri!(home)), e));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Redirect::to(uri!(home)))
|
||||
}
|
||||
|
||||
#[post("/unsubscribe", data = "<peer>")]
|
||||
pub async fn unsubscribe_form(
|
||||
db: &State<Database>,
|
||||
peer: Form<PeerForm>,
|
||||
) -> Result<Redirect, Flash<Redirect>> {
|
||||
if let Err(e) = utils::validate_public_key(&peer.public_key) {
|
||||
let validation_err_msg = format!("Public key {} is invalid: {}", &peer.public_key, e);
|
||||
warn!("{}", validation_err_msg);
|
||||
return Err(Flash::error(Redirect::to(uri!(home)), validation_err_msg));
|
||||
} else {
|
||||
info!("Public key {} is valid", &peer.public_key);
|
||||
match sbot::unfollow_if_following(&peer.public_key).await {
|
||||
Ok(_) => {
|
||||
// Remove the peer from the database.
|
||||
if db.remove_peer(&peer.public_key).is_ok() {
|
||||
info!(
|
||||
"Removed peer {} from 'peers' database tree",
|
||||
&peer.public_key
|
||||
);
|
||||
} else {
|
||||
warn!(
|
||||
"Failed to remove peer {} from 'peers' database tree",
|
||||
&peer.public_key
|
||||
);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("{}", e);
|
||||
return Err(Flash::error(Redirect::to(uri!(home)), e));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Redirect::to(uri!(home)))
|
||||
}
|
||||
|
||||
/*
|
||||
#[post("/subscribe", data = "<peer>")]
|
||||
pub async fn subscribe_form(peer: Form<PeerForm>) -> Result<Redirect, Flash<Redirect>> {
|
||||
if let Err(e) = utils::validate_public_key(&peer.public_key) {
|
||||
let validation_err_msg = format!("Public key {} is invalid: {}", &peer.public_key, e);
|
||||
warn!("{}", validation_err_msg);
|
||||
return Err(Flash::error(Redirect::to(uri!(home)), validation_err_msg));
|
||||
} else {
|
||||
info!("Public key {} is valid", &peer.public_key);
|
||||
sbot::follow_if_not_following(&peer.public_key).await;
|
||||
}
|
||||
|
||||
Ok(Redirect::to(uri!(home)))
|
||||
}
|
||||
|
||||
#[post("/unsubscribe", data = "<peer>")]
|
||||
pub async fn unsubscribe_form(peer: Form<PeerForm>) -> Result<Redirect, Flash<Redirect>> {
|
||||
if let Err(e) = utils::validate_public_key(&peer.public_key) {
|
||||
let validation_err_msg = format!("Public key {} is invalid: {}", &peer.public_key, e);
|
||||
warn!("{}", validation_err_msg);
|
||||
return Err(Flash::error(Redirect::to(uri!(home)), validation_err_msg));
|
||||
} else {
|
||||
info!("Public key {} is valid", &peer.public_key);
|
||||
sbot::unfollow_if_following(&peer.public_key).await;
|
||||
}
|
||||
|
||||
Ok(Redirect::to(uri!(home)))
|
||||
}
|
||||
*/
|
130
part_4_posts_streams/src/sbot.rs
Normal file
130
part_4_posts_streams/src/sbot.rs
Normal file
@ -0,0 +1,130 @@
|
||||
use std::env;
|
||||
|
||||
use golgi::{api::friends::RelationshipQuery, sbot::Keystore, Sbot};
|
||||
use log::{info, warn};
|
||||
|
||||
/// Initialise a connection to a Scuttlebutt server.
|
||||
pub async fn init_sbot() -> Result<Sbot, String> {
|
||||
let go_sbot_port = env::var("GO_SBOT_PORT").unwrap_or_else(|_| "8021".to_string());
|
||||
|
||||
let keystore = Keystore::GoSbot;
|
||||
let ip_port = Some(format!("127.0.0.1:{}", go_sbot_port));
|
||||
let net_id = None;
|
||||
|
||||
Sbot::init(keystore, ip_port, net_id)
|
||||
.await
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
/// Return the public key of the local sbot instance.
|
||||
pub async fn whoami() -> Result<String, String> {
|
||||
let mut sbot = init_sbot().await?;
|
||||
|
||||
sbot.whoami().await.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
/// Check follow status.
|
||||
///
|
||||
/// Is peer A (`public_key_a`) following peer B (`public_key_b`)?
|
||||
pub async fn is_following(public_key_a: &str, public_key_b: &str) -> Result<String, String> {
|
||||
let mut sbot = init_sbot().await?;
|
||||
|
||||
let query = RelationshipQuery {
|
||||
source: public_key_a.to_string(),
|
||||
dest: public_key_b.to_string(),
|
||||
};
|
||||
|
||||
sbot.friends_is_following(query)
|
||||
.await
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
/// Follow a peer.
|
||||
pub async fn follow_peer(public_key: &str) -> Result<String, String> {
|
||||
let mut sbot = init_sbot().await?;
|
||||
|
||||
sbot.follow(public_key).await.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
/// Unfollow a peer.
|
||||
pub async fn unfollow_peer(public_key: &str) -> Result<String, String> {
|
||||
let mut sbot = init_sbot().await?;
|
||||
|
||||
sbot.unfollow(public_key).await.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
/// Return the name (self-identifier) for the peer associated with the given
|
||||
/// public key.
|
||||
///
|
||||
/// The public key of the peer will be returned if a name is not found.
|
||||
pub async fn get_name(public_key: &str) -> Result<String, String> {
|
||||
let mut sbot = init_sbot().await?;
|
||||
|
||||
sbot.get_name(public_key).await.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
/// Check the follow status of a remote peer and follow them if not already
|
||||
/// following.
|
||||
pub async fn follow_if_not_following(remote_peer: &str) -> Result<(), String> {
|
||||
if let Ok(whoami) = whoami().await {
|
||||
match is_following(&whoami, remote_peer).await {
|
||||
Ok(status) if status.as_str() == "false" => match follow_peer(remote_peer).await {
|
||||
Ok(_) => {
|
||||
info!("Followed peer {}", &remote_peer);
|
||||
Ok(())
|
||||
}
|
||||
Err(e) => {
|
||||
let err_msg = format!("Failed to follow peer {}: {}", &remote_peer, e);
|
||||
warn!("{}", err_msg);
|
||||
Err(err_msg)
|
||||
}
|
||||
},
|
||||
Ok(status) if status.as_str() == "true" => {
|
||||
info!(
|
||||
"Already following peer {}. No further action taken",
|
||||
&remote_peer
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
_ => Err(
|
||||
"Failed to determine follow status: received unrecognised response from local sbot"
|
||||
.to_string(),
|
||||
),
|
||||
}
|
||||
} else {
|
||||
let err_msg = String::from("Received an error during `whoami` RPC call. Please ensure the go-sbot is running and try again");
|
||||
warn!("{}", err_msg);
|
||||
Err(err_msg)
|
||||
}
|
||||
}
|
||||
|
||||
/// Check the follow status of a remote peer and unfollow them if already
|
||||
/// following.
|
||||
pub async fn unfollow_if_following(remote_peer: &str) -> Result<(), String> {
|
||||
if let Ok(whoami) = whoami().await {
|
||||
match is_following(&whoami, remote_peer).await {
|
||||
Ok(status) if status.as_str() == "true" => {
|
||||
info!("Unfollowing peer {}", &remote_peer);
|
||||
match unfollow_peer(remote_peer).await {
|
||||
Ok(_) => {
|
||||
info!("Unfollowed peer {}", &remote_peer);
|
||||
Ok(())
|
||||
}
|
||||
Err(e) => {
|
||||
let err_msg = format!("Failed to unfollow peer {}: {}", &remote_peer, e);
|
||||
warn!("{}", err_msg);
|
||||
Err(err_msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => Err(
|
||||
"Failed to determine follow status: received unrecognised response from local sbot"
|
||||
.to_string(),
|
||||
),
|
||||
}
|
||||
} else {
|
||||
let err_msg = String::from("Received an error during `whoami` RPC call. Please ensure the go-sbot is running and try again");
|
||||
warn!("{}", err_msg);
|
||||
Err(err_msg)
|
||||
}
|
||||
}
|
32
part_4_posts_streams/src/utils.rs
Normal file
32
part_4_posts_streams/src/utils.rs
Normal file
@ -0,0 +1,32 @@
|
||||
//! Public key validation.
|
||||
|
||||
/// Ensure that the given public key is a valid ed25519 key.
|
||||
///
|
||||
/// Return an error string if the key is invalid.
|
||||
pub fn validate_public_key(public_key: &str) -> Result<(), String> {
|
||||
// Ensure the ID starts with the correct sigil link.
|
||||
if !public_key.starts_with('@') {
|
||||
return Err("expected '@' sigil as first character".to_string());
|
||||
}
|
||||
|
||||
// Find the dot index denoting the start of the algorithm definition tag.
|
||||
let dot_index = match public_key.rfind('.') {
|
||||
Some(index) => index,
|
||||
None => return Err("no dot index was found".to_string()),
|
||||
};
|
||||
|
||||
// Check the hashing algorithm (must end with ".ed25519").
|
||||
if !&public_key.ends_with(".ed25519") {
|
||||
return Err("hashing algorithm must be ed25519".to_string());
|
||||
}
|
||||
|
||||
// Obtain the base64 portion (substring) of the public key.
|
||||
let base64_str = &public_key[1..dot_index];
|
||||
|
||||
// Ensure the length of the base64 encoded ed25519 public key is correct.
|
||||
if base64_str.len() != 44 {
|
||||
return Err("base64 data length is incorrect".to_string());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
Reference in New Issue
Block a user