From 020d18731b02fa42b867fca234cf5e4e36fc4815 Mon Sep 17 00:00:00 2001 From: glyph Date: Wed, 2 Mar 2022 08:58:54 +0200 Subject: [PATCH] add block list and implement (un)follow and (un)block --- peach-web/src/context/sbot.rs | 97 ++++- peach-web/src/router.rs | 1 + peach-web/src/routes/scuttlebutt.rs | 348 +++++++++++++----- .../templates/scuttlebutt/profile.html.tera | 20 +- .../templates/scuttlebutt/search.html.tera | 4 +- 5 files changed, 360 insertions(+), 110 deletions(-) diff --git a/peach-web/src/context/sbot.rs b/peach-web/src/context/sbot.rs index 92e912f..ac40d85 100644 --- a/peach-web/src/context/sbot.rs +++ b/peach-web/src/context/sbot.rs @@ -26,7 +26,98 @@ pub async fn init_sbot_with_config( // CONTEXT STRUCTS AND BUILDERS -// peers who follow the local account +// peers who are blocked by the local account +#[derive(Debug, Serialize)] +pub struct BlocksContext { + pub back: Option, + pub flash_name: Option, + pub flash_msg: Option, + pub title: Option, + pub theme: Option, + pub sbot_config: Option, + pub sbot_status: Option, + pub peers: Option>>, +} + +impl BlocksContext { + pub fn default() -> Self { + BlocksContext { + back: Some("/scuttlebutt/peers".to_string()), + flash_name: None, + flash_msg: None, + title: Some("Blocks".to_string()), + theme: None, + sbot_config: None, + sbot_status: None, + peers: None, + } + } + + pub async fn build() -> Result { + let mut context = Self::default(); + + // retrieve current ui theme + context.theme = Some(utils::get_theme()); + + // retrieve go-sbot systemd process status + let sbot_status = SbotStatus::read()?; + + // we only want to try and interact with the sbot if it's active + if sbot_status.state == Some("active".to_string()) { + // retrieve latest go-sbot configuration parameters + let sbot_config = SbotConfig::read().ok(); + + let mut sbot_client = init_sbot_with_config(&sbot_config).await?; + + let blocks = sbot_client.get_blocks().await?; + + // we'll use this to store the profile info for each peer who follows us + let mut peer_info = Vec::new(); + + if !blocks.is_empty() { + for peer in blocks.iter() { + // trim whitespace (including newline characters) and + // remove the inverted-commas around the id + let key = peer.trim().replace('"', ""); + // retrieve the profile info for the given peer + let mut info = sbot_client.get_profile_info(&key).await?; + // insert the public key of the peer into the info hashmap + info.insert("id".to_string(), key.to_string()); + // retrieve the profile image blob id for the given peer + if let Some(blob_id) = info.get("image") { + // look-up the path for the image blob + if let Ok(blob_path) = blobs::get_blob_path(&blob_id) { + // insert the image blob path of the peer into the info hashmap + info.insert("blob_path".to_string(), blob_path.to_string()); + // check if the blob is in the blobstore + // set a flag in the info hashmap + match utils::blob_is_stored_locally(&blob_path).await { + Ok(exists) if exists == true => { + info.insert("blob_exists".to_string(), "true".to_string()) + } + _ => info.insert("blob_exists".to_string(), "false".to_string()), + }; + } + } + // push profile info to peer_list vec + peer_info.push(info) + } + + context.peers = Some(peer_info) + } + } else { + // the sbot is not currently active; return a helpful message + context.flash_name = Some("warning".to_string()); + context.flash_msg = Some("The Sbot is currently inactive. As a result, peer data cannot be retrieved. Visit the Scuttlebutt settings menu to start the Sbot and then try again".to_string()); + } + + context.sbot_status = Some(sbot_status); + + Ok(context) + } +} + +// peers who are followed by the local account #[derive(Debug, Serialize)] pub struct FollowsContext { pub back: Option, @@ -108,7 +199,7 @@ impl FollowsContext { } else { // the sbot is not currently active; return a helpful message context.flash_name = Some("warning".to_string()); - context.flash_msg = Some("The Sbot is currently inactive. As a result, profile data cannot be retrieved. Visit the Scuttlebutt settings menu to start the Sbot and then try again".to_string()); + context.flash_msg = Some("The Sbot is currently inactive. As a result, peer data cannot be retrieved. Visit the Scuttlebutt settings menu to start the Sbot and then try again".to_string()); } context.sbot_status = Some(sbot_status); @@ -214,7 +305,7 @@ impl FriendsContext { } else { // the sbot is not currently active; return a helpful message context.flash_name = Some("warning".to_string()); - context.flash_msg = Some("The Sbot is currently inactive. As a result, profile data cannot be retrieved. Visit the Scuttlebutt settings menu to start the Sbot and then try again".to_string()); + context.flash_msg = Some("The Sbot is currently inactive. As a result, peer data cannot be retrieved. Visit the Scuttlebutt settings menu to start the Sbot and then try again".to_string()); } context.sbot_status = Some(sbot_status); diff --git a/peach-web/src/router.rs b/peach-web/src/router.rs index 9ad2ae6..8e9eb75 100644 --- a/peach-web/src/router.rs +++ b/peach-web/src/router.rs @@ -84,6 +84,7 @@ pub fn mount_peachpub_routes(rocket: Rocket) -> Rocket { follow, unfollow, block, + unblock, publish, ], ) diff --git a/peach-web/src/routes/scuttlebutt.rs b/peach-web/src/routes/scuttlebutt.rs index 005817d..462edcf 100644 --- a/peach-web/src/routes/scuttlebutt.rs +++ b/peach-web/src/routes/scuttlebutt.rs @@ -16,7 +16,7 @@ use rocket_dyn_templates::{tera::Context, Template}; use crate::{ context::{ sbot, - sbot::{FollowsContext, FriendsContext, ProfileContext}, + sbot::{BlocksContext, FollowsContext, FriendsContext, ProfileContext}, }, routes::authentication::Authenticated, utils, @@ -141,7 +141,7 @@ pub fn private(flash: Option, _auth: Authenticated) -> Template { context.flash_msg = Some(flash.message().to_string()); }; - Template::render("scuttlebutt/messages", &context) + Template::render("scuttlebutt/private", &context) } // HELPERS AND ROUTES FOR /search @@ -172,55 +172,63 @@ pub fn search(flash: Option, _auth: Authenticated) -> Template { } #[derive(Debug, FromForm)] -pub struct Search { +pub struct Peer { + // public key pub public_key: String, } -/// Accept the peer search form and redirect to the profile for that peer. -#[post("/search", data = "")] -pub async fn search_post(search: Form, _auth: Authenticated) -> Flash { - let public_key = &search.public_key; - - let search_url = "/scuttlebutt/search"; - - // validate the key before redirecting to profile url - +fn validate_public_key(public_key: &str, redirect_url: String) -> Result<(), Flash> { // ensure the id starts with the correct sigil link if !public_key.starts_with('@') { - return Flash::error( - Redirect::to(search_url), + return Err(Flash::error( + Redirect::to(redirect_url), "Invalid key: expected '@' sigil as first character", - ); + )); } // find the dot index denoting the start of the algorithm definition tag let dot_index = match public_key.rfind('.') { Some(index) => index, None => { - return Flash::error( - Redirect::to(search_url), + return Err(Flash::error( + Redirect::to(redirect_url), "Invalid key: no dot index was found", - ) + )) } }; // check hashing algorithm (must end with ".ed25519") if !&public_key.ends_with(".ed25519") { - return Flash::error( - Redirect::to(search_url), + return Err(Flash::error( + Redirect::to(redirect_url), "Invalid key: hashing algorithm must be ed25519", - ); + )); } // obtain the base64 portion (substring) of the public key let base64_str = &public_key[1..dot_index]; // length of a base64 encoded ed25519 public key - if !base64_str.len() == 44 { - return Flash::error( - Redirect::to(search_url), + if base64_str.len() != 44 { + return Err(Flash::error( + Redirect::to(redirect_url), "Invalid key: base64 data length is incorrect", - ); + )); + } + + Ok(()) +} + +/// Accept the peer search form and redirect to the profile for that peer. +#[post("/search", data = "")] +pub async fn search_post(peer: Form, _auth: Authenticated) -> Flash { + let public_key = &peer.public_key; + + let search_url = "/scuttlebutt/search".to_string(); + + // validate the key before redirecting to profile url + if let Err(flash) = validate_public_key(&public_key, search_url) { + return flash; } // key has not been validated and we can redirect to the profile page @@ -276,8 +284,15 @@ pub async fn publish(post: Form, _auth: Authenticated) -> Flash let url = uri!("/scuttlebutt", profile(None::)); // retrieve go-sbot systemd process status - // TODO: handle unwrap properly - let sbot_status = SbotStatus::read().unwrap(); + let sbot_status = match SbotStatus::read() { + Ok(status) => status, + Err(e) => { + return Flash::error( + Redirect::to(url), + format!("Failed to read sbot status: {}", e), + ) + } + }; // we only want to try and interact with the sbot if it's active if sbot_status.state == Some("active".to_string()) { @@ -310,25 +325,53 @@ pub async fn publish(post: Form, _auth: Authenticated) -> Flash // HELPERS AND ROUTES FOR /follow -#[derive(Debug, FromForm)] -pub struct PublicKey { - pub key: String, -} - /// Follow a Scuttlebutt profile specified by the given public key. /// Redirects to the appropriate profile page with a flash message describing /// the outcome of the action (may be successful or unsuccessful). -#[post("/follow", data = "")] -pub fn follow(pub_key: Form, _auth: Authenticated) -> Flash { - let public_key = &pub_key.key; - // perform the sbotcli follow action using &pub_key.0 - // if successful, redirect to profile page with provided public key and flash "success" - // if error, redirect to profile page with provided public key and flash "error" - // redirect to the profile template with provided public key - let profile_url = uri!("/scuttlebutt", profile(Some(public_key))); - let success_msg = format!("Followed {}", public_key); +#[post("/follow", data = "")] +pub async fn follow(peer: Form, _auth: Authenticated) -> Flash { + let public_key = &peer.public_key; - Flash::success(Redirect::to(profile_url), success_msg) + let url = uri!("/scuttlebutt", profile(Some(public_key))); + + // retrieve go-sbot systemd process status + let sbot_status = match SbotStatus::read() { + Ok(status) => status, + Err(e) => { + return Flash::error( + Redirect::to(url), + format!("Failed to read sbot status: {}", e), + ) + } + }; + + // we only want to try and interact with the sbot if it's active + if sbot_status.state == Some("active".to_string()) { + // retrieve latest go-sbot configuration parameters + let sbot_config = SbotConfig::read().ok(); + + // initialise sbot connection with ip:port and shscap from config file + match sbot::init_sbot_with_config(&sbot_config).await { + Ok(mut sbot_client) => { + debug!("Following Scuttlebutt peer"); + match sbot_client.follow(public_key).await { + Ok(_) => Flash::success(Redirect::to(url), format!("Followed peer")), + Err(e) => { + Flash::error(Redirect::to(url), format!("Failed to follow peer: {}", e)) + } + } + } + Err(e) => Flash::error( + Redirect::to(url), + format!("Failed to initialise sbot: {}", e), + ), + } + } else { + return Flash::warning( + Redirect::to(url), + "The Sbot is currently inactive. As a result, follow messages cannot be published. Visit the Scuttlebutt settings menu to start the Sbot and then try again", + ); + } } // HELPERS AND ROUTES FOR /unfollow @@ -336,17 +379,50 @@ pub fn follow(pub_key: Form, _auth: Authenticated) -> Flash /// Unfollow a Scuttlebutt profile specified by the given public key. /// Redirects to the appropriate profile page with a flash message describing /// the outcome of the action (may be successful or unsuccessful). -#[post("/unfollow", data = "")] -pub fn unfollow(pub_key: Form, _auth: Authenticated) -> Flash { - let public_key = &pub_key.key; - // perform the sbotcli unfollow action using &pub_key.0 - // if successful, redirect to profile page with provided public key and flash "success" - // if error, redirect to profile page with provided public key and flash "error" - // redirect to the profile template with provided public key - let profile_url = uri!("/scuttlebutt", profile(Some(public_key))); - let success_msg = format!("Unfollowed {}", public_key); +#[post("/unfollow", data = "")] +pub async fn unfollow(peer: Form, _auth: Authenticated) -> Flash { + let public_key = &peer.public_key; - Flash::success(Redirect::to(profile_url), success_msg) + let url = uri!("/scuttlebutt", profile(Some(public_key))); + + // retrieve go-sbot systemd process status + let sbot_status = match SbotStatus::read() { + Ok(status) => status, + Err(e) => { + return Flash::error( + Redirect::to(url), + format!("Failed to read sbot status: {}", e), + ) + } + }; + + // we only want to try and interact with the sbot if it's active + if sbot_status.state == Some("active".to_string()) { + // retrieve latest go-sbot configuration parameters + let sbot_config = SbotConfig::read().ok(); + + // initialise sbot connection with ip:port and shscap from config file + match sbot::init_sbot_with_config(&sbot_config).await { + Ok(mut sbot_client) => { + debug!("Unfollowing Scuttlebutt peer"); + match sbot_client.unfollow(public_key).await { + Ok(_) => Flash::success(Redirect::to(url), format!("Unfollowed peer")), + Err(e) => { + Flash::error(Redirect::to(url), format!("Failed to unfollow peer: {}", e)) + } + } + } + Err(e) => Flash::error( + Redirect::to(url), + format!("Failed to initialise sbot: {}", e), + ), + } + } else { + return Flash::warning( + Redirect::to(url), + "The Sbot is currently inactive. As a result, follow messages cannot be published. Visit the Scuttlebutt settings menu to start the Sbot and then try again", + ); + } } // HELPERS AND ROUTES FOR /block @@ -354,17 +430,101 @@ pub fn unfollow(pub_key: Form, _auth: Authenticated) -> Flash, _auth: Authenticated) -> Flash { - let public_key = &pub_key.key; - // perform the sbotcli block action using &pub_key.0 - // if successful, redirect to profile page with provided public key and flash "success" - // if error, redirect to profile page with provided public key and flash "error" - // redirect to the profile template with provided public key - let profile_url = uri!("/scuttlebutt", profile(Some(public_key))); - let success_msg = format!("Blocked {}", public_key); +#[post("/block", data = "")] +pub async fn block(peer: Form, _auth: Authenticated) -> Flash { + let public_key = &peer.public_key; - Flash::success(Redirect::to(profile_url), success_msg) + let url = uri!("/scuttlebutt", profile(Some(public_key))); + + // retrieve go-sbot systemd process status + let sbot_status = match SbotStatus::read() { + Ok(status) => status, + Err(e) => { + return Flash::error( + Redirect::to(url), + format!("Failed to read sbot status: {}", e), + ) + } + }; + + // we only want to try and interact with the sbot if it's active + if sbot_status.state == Some("active".to_string()) { + // retrieve latest go-sbot configuration parameters + let sbot_config = SbotConfig::read().ok(); + + // initialise sbot connection with ip:port and shscap from config file + match sbot::init_sbot_with_config(&sbot_config).await { + Ok(mut sbot_client) => { + debug!("Blocking Scuttlebutt peer"); + match sbot_client.block(public_key).await { + Ok(_) => Flash::success(Redirect::to(url), format!("Blocked peer")), + Err(e) => { + Flash::error(Redirect::to(url), format!("Failed to block peer: {}", e)) + } + } + } + Err(e) => Flash::error( + Redirect::to(url), + format!("Failed to initialise sbot: {}", e), + ), + } + } else { + return Flash::warning( + Redirect::to(url), + "The Sbot is currently inactive. As a result, follow messages cannot be published. Visit the Scuttlebutt settings menu to start the Sbot and then try again", + ); + } +} + +// HELPERS AND ROUTES FOR /unblock + +/// Unblock a Scuttlebutt profile specified by the given public key. +/// Redirects to the appropriate profile page with a flash message describing +/// the outcome of the action (may be successful or unsuccessful). +#[post("/unblock", data = "")] +pub async fn unblock(peer: Form, _auth: Authenticated) -> Flash { + let public_key = &peer.public_key; + + let url = uri!("/scuttlebutt", profile(Some(public_key))); + + // retrieve go-sbot systemd process status + let sbot_status = match SbotStatus::read() { + Ok(status) => status, + Err(e) => { + return Flash::error( + Redirect::to(url), + format!("Failed to read sbot status: {}", e), + ) + } + }; + + // we only want to try and interact with the sbot if it's active + if sbot_status.state == Some("active".to_string()) { + // retrieve latest go-sbot configuration parameters + let sbot_config = SbotConfig::read().ok(); + + // initialise sbot connection with ip:port and shscap from config file + match sbot::init_sbot_with_config(&sbot_config).await { + Ok(mut sbot_client) => { + debug!("Unblocking Scuttlebutt peer"); + match sbot_client.unblock(public_key).await { + Ok(_) => Flash::success(Redirect::to(url), format!("Unblocked peer")), + Err(e) => { + Flash::error(Redirect::to(url), format!("Failed to unblock peer: {}", e)) + } + } + } + Err(e) => Flash::error( + Redirect::to(url), + format!("Failed to initialise sbot: {}", e), + ), + } + } else { + return Flash::warning( + Redirect::to(url), + "The Sbot is currently inactive. As a result, follow messages cannot be published. Visit the Scuttlebutt settings menu to start the Sbot and then try again", + ); + } } // ROUTES FOR /profile @@ -644,45 +804,33 @@ pub async fn follows(flash: Option>, _auth: Authenticated) -> T // HELPERS AND ROUTES FOR /blocks -#[derive(Debug, Serialize)] -pub struct BlocksContext { - pub back: Option, - pub flash_name: Option, - pub flash_msg: Option, - pub title: Option, - pub theme: Option, -} +/// A list of blocks (peers we've blocked previously), with each list item +/// displaying the name, image and public key of the peer. +#[get("/blocks")] +pub async fn blocks(flash: Option>, _auth: Authenticated) -> Template { + // build the blocks context object + let context = BlocksContext::build().await; -impl BlocksContext { - pub fn build() -> BlocksContext { - BlocksContext { - back: None, - flash_name: None, - flash_msg: None, - title: None, - theme: None, + match context { + // we were able to build the context without errors + Ok(mut context) => { + // check to see if there is a flash message to display + if let Some(flash) = flash { + // add flash message contents to the context object + context.flash_name = Some(flash.kind().to_string()); + context.flash_msg = Some(flash.message().to_string()); + }; + + Template::render("scuttlebutt/peers_list", &context) + } + // an error occurred while building the context + Err(e) => { + // build the default context and pass along the error message + let mut context = BlocksContext::default(); + context.flash_name = Some("error".to_string()); + context.flash_msg = Some(e.to_string()); + + Template::render("scuttlebutt/peers_list", &context) } } } - -/// A list of blocks (peers we've blocked previously), with each list item displaying the name, image and public -/// key of the peer. -#[get("/blocks")] -pub fn blocks(flash: Option, _auth: Authenticated) -> Template { - // retrieve current ui theme - let theme = utils::get_theme(); - - let mut context = BlocksContext::build(); - context.back = Some("/scuttlebutt/peers".to_string()); - context.title = Some("Blocks".to_string()); - context.theme = Some(theme); - - // check to see if there is a flash message to display - if let Some(flash) = flash { - // add flash message contents to the context object - context.flash_name = Some(flash.kind().to_string()); - context.flash_msg = Some(flash.message().to_string()); - }; - - Template::render("scuttlebutt/peers_list", &context) -} diff --git a/peach-web/templates/scuttlebutt/profile.html.tera b/peach-web/templates/scuttlebutt/profile.html.tera index de40580..9237134 100644 --- a/peach-web/templates/scuttlebutt/profile.html.tera +++ b/peach-web/templates/scuttlebutt/profile.html.tera @@ -38,16 +38,28 @@
{% if following == false %} - Follow +
+ + +
{% elif following == true %} - Unfollow +
+ + +
{% else %}

Unable to determine follow state

{% endif %} {% if blocking == false %} - Block +
+ + +
{% elif blocking == true %} - Unblock +
+ + +
{% else %}

Unable to determine block state

{% endif %} diff --git a/peach-web/templates/scuttlebutt/search.html.tera b/peach-web/templates/scuttlebutt/search.html.tera index e546737..d5e508d 100644 --- a/peach-web/templates/scuttlebutt/search.html.tera +++ b/peach-web/templates/scuttlebutt/search.html.tera @@ -1,13 +1,11 @@ {%- extends "nav" -%} {%- block card %} - {# ASSIGN VARIABLES #} - {# ---------------- #}
- +