add block list and implement (un)follow and (un)block

This commit is contained in:
glyph 2022-03-02 08:58:54 +02:00
parent a38394054d
commit 020d18731b
5 changed files with 360 additions and 110 deletions

View File

@ -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<String>,
pub flash_name: Option<String>,
pub flash_msg: Option<String>,
pub title: Option<String>,
pub theme: Option<String>,
pub sbot_config: Option<SbotConfig>,
pub sbot_status: Option<SbotStatus>,
pub peers: Option<Vec<HashMap<String, String>>>,
}
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<Self, PeachWebError> {
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<String>,
@ -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);

View File

@ -84,6 +84,7 @@ pub fn mount_peachpub_routes(rocket: Rocket<Build>) -> Rocket<Build> {
follow,
unfollow,
block,
unblock,
publish,
],
)

View File

@ -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<FlashMessage>, _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<FlashMessage>, _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 = "<search>")]
pub async fn search_post(search: Form<Search>, _auth: Authenticated) -> Flash<Redirect> {
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<Redirect>> {
// 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 = "<peer>")]
pub async fn search_post(peer: Form<Peer>, _auth: Authenticated) -> Flash<Redirect> {
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<Post>, _auth: Authenticated) -> Flash<Redirect>
let url = uri!("/scuttlebutt", profile(None::<String>));
// 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<Post>, _auth: Authenticated) -> Flash<Redirect>
// 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_key>")]
pub fn follow(pub_key: Form<PublicKey>, _auth: Authenticated) -> Flash<Redirect> {
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 = "<peer>")]
pub async fn follow(peer: Form<Peer>, _auth: Authenticated) -> Flash<Redirect> {
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<PublicKey>, _auth: Authenticated) -> Flash<Redirect>
/// 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_key>")]
pub fn unfollow(pub_key: Form<PublicKey>, _auth: Authenticated) -> Flash<Redirect> {
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 = "<peer>")]
pub async fn unfollow(peer: Form<Peer>, _auth: Authenticated) -> Flash<Redirect> {
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<PublicKey>, _auth: Authenticated) -> Flash<Redirec
/// Block 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("/block", data = "<pub_key>")]
pub fn block(pub_key: Form<PublicKey>, _auth: Authenticated) -> Flash<Redirect> {
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 = "<peer>")]
pub async fn block(peer: Form<Peer>, _auth: Authenticated) -> Flash<Redirect> {
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 = "<peer>")]
pub async fn unblock(peer: Form<Peer>, _auth: Authenticated) -> Flash<Redirect> {
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<FlashMessage<'_>>, _auth: Authenticated) -> T
// HELPERS AND ROUTES FOR /blocks
#[derive(Debug, Serialize)]
pub struct BlocksContext {
pub back: Option<String>,
pub flash_name: Option<String>,
pub flash_msg: Option<String>,
pub title: Option<String>,
pub theme: Option<String>,
}
/// 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<FlashMessage<'_>>, _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<FlashMessage>, _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)
}

View File

@ -38,16 +38,28 @@
<!-- TODO: each of these buttons needs to be a form with a public key -->
<div id="buttons" style="margin-top: 2rem;">
{% if following == false %}
<a id="followPeer" class="button button-primary center" href="/scuttlebutt/follow" title="Follow Peer">Follow</a>
<form id="followForm" action="/scuttlebutt/follow" method="post">
<input type="hidden" id="publicKey" name="public_key" value="{{ id }}">
<input id="followPeer" class="button button-primary center" type="submit" title="Follow Peer" value="Follow">
</form>
{% elif following == true %}
<a id="unfollowPeer" class="button button-primary center" href="/scuttlebutt/unfollow" title="Unfollow Peer">Unfollow</a>
<form id="unfollowForm" action="/scuttlebutt/unfollow" method="post">
<input type="hidden" id="publicKey" name="public_key" value="{{ id }}">
<input id="unfollowPeer" class="button button-primary center" type="submit" title="Unfollow Peer" value="Unfollow">
</form>
{% else %}
<p>Unable to determine follow state</p>
{% endif %}
{% if blocking == false %}
<a id="blockPeer" class="button button-warning center" href="/scuttlebutt/block" title="Block Peer">Block</a>
<form id="blockForm" action="/scuttlebutt/block" method="post">
<input type="hidden" id="publicKey" name="public_key" value="{{ id }}">
<input id="blockPeer" class="button button-primary center" type="submit" title="Block Peer" value="Block">
</form>
{% elif blocking == true %}
<a id="unblockPeer" class="button button-warning center" href="/scuttlebutt/unblock" title="Unblock Peer">Unblock</a>
<form id="unblockForm" action="/scuttlebutt/unblock" method="post">
<input type="hidden" id="publicKey" name="public_key" value="{{ id }}">
<input id="unblockPeer" class="button button-primary center" type="submit" title="Unblock Peer" value="Unblock">
</form>
{% else %}
<p>Unable to determine block state</p>
{% endif %}

View File

@ -1,13 +1,11 @@
{%- extends "nav" -%}
{%- block card %}
{# ASSIGN VARIABLES #}
{# ---------------- #}
<!-- PEER SEARCH FORM -->
<div class="card center">
<form id="sbotConfig" class="center" action="/scuttlebutt/search" method="post">
<div class="center" style="display: flex; flex-direction: column; margin-bottom: 2rem;" title="Public key (ID) of a peer">
<label for="publicKey" class="label-small font-gray">PUBLIC KEY</label>
<input type="text" id="publicKey" name="public_key" placeholder="@xYz...=.sha256">
<input type="text" id="publicKey" name="public_key" placeholder="@xYz...=.ed25519" autofocus>
</div>
<!-- BUTTONS -->
<input id="search" class="button button-primary center" type="submit" title="Search for peer" value="Search">