add profile update template and route handler

This commit is contained in:
glyph 2022-03-22 16:11:52 +02:00
parent 3e918f66cf
commit 084af1b486
3 changed files with 279 additions and 18 deletions

View File

@ -124,6 +124,14 @@ pub fn mount_peachpub_routes(request: &Request) -> Response {
Response::html(routes::scuttlebutt::profile::build_template(request, None))
},
(GET) (/scuttlebutt/profile/update) => {
Response::html(routes::scuttlebutt::profile_update::build_template(request))
},
(POST) (/scuttlebutt/profile/update) => {
routes::scuttlebutt::profile_update::handle_form(request)
},
(GET) (/scuttlebutt/profile/{ssb_id: String}) => {
debug!("ssb_id: {}", ssb_id);
Response::html(routes::scuttlebutt::profile::build_template(request, Some(ssb_id)))

View File

@ -0,0 +1,169 @@
use maud::{html, PreEscaped};
use peach_lib::sbot::SbotStatus;
use rouille::{input::post::BufferedFile, post_input, try_or_400, Request, Response};
use crate::{
templates,
utils::{
flash::{FlashRequest, FlashResponse},
sbot,
sbot::Profile,
},
};
// ROUTE: /scuttlebutt/profile/update
fn parse_profile_info(profile: Profile) -> (String, String, String) {
let id = match profile.id {
Some(id) => id,
_ => "Public key unavailable".to_string(),
};
let name = match profile.name {
Some(name) => name,
_ => "Name unavailable".to_string(),
};
let description = match profile.description {
Some(description) => description,
_ => "Description unavailable".to_string(),
};
(id, name, description)
}
/// Scuttlebutt profile update template builder.
///
/// Serve a form for the purpose of updating the name, description and picture
/// for the local Scuttlebutt profile.
pub fn build_template(request: &Request) -> PreEscaped<String> {
// check for flash cookies; will be (None, None) if no flash cookies are found
let (flash_name, flash_msg) = request.retrieve_flash();
let profile_update_template = match SbotStatus::read() {
Ok(status) if status.state == Some("active".to_string()) => {
// retrieve the local profile info
match sbot::get_profile_info(None) {
Ok(profile) => {
let (id, name, description) = parse_profile_info(profile);
// render the scuttlebutt profile update form
html! {
(PreEscaped("<!-- SSB PROFILE UPDATE FORM -->"))
div class="card card-wide center" {
form id="profileInfo" class="center" enctype="multipart/form-data" action="/scuttlebutt/profile/update" method="post" {
div style="display: flex; flex-direction: column" {
label for="name" class="label-small font-gray" {
"NAME"
}
input style="margin-bottom: 1rem;" type="text" id="name" name="new_name" placeholder="Choose a name for your profile..." value=(name);
label for="description" class="label-small font-gray" {
"DESCRIPTION"
}
textarea id="description" class="message-input" style="margin-bottom: 1rem;" name="new_description" placeholder="Write a description for your profile..." {
(description)
}
label for="image" class="label-small font-gray" {
"IMAGE"
}
input type="file" id="fileInput" class="font-normal" name="image";
}
input type="hidden" name="id" value=(id);
input type="hidden" name="current_name" value=(name);
input type="hidden" name="current_description" value=(description);
div id="buttonDiv" style="margin-top: 2rem;" {
input id="updateProfile" class="button button-primary center" title="Publish" type="submit" value="Publish";
a class="button button-secondary center" href="/scuttlebutt/profile" title="Cancel" { "Cancel" }
}
}
// render flash message if cookies were found in the request
@if let (Some(name), Some(msg)) = (flash_name, flash_msg) {
(PreEscaped("<!-- FLASH MESSAGE -->"))
(templates::flash::build_template(name, msg))
}
}
}
}
Err(e) => {
// render the sbot error template with the error message
templates::error::build_template(e.to_string())
}
}
}
_ => {
// the sbot is not active; render a message instead of the form
templates::inactive::build_template("Profile is unavailable.")
}
};
let body = templates::nav::build_template(
profile_update_template,
"Profile",
Some("/scuttlebutt/profile"),
);
templates::base::build_template(body)
}
/// Update the name, description and picture for the local Scuttlebutt profile.
///
/// Redirects to profile page of the PeachCloud local identity with a flash
/// message describing the outcome of the action (may be successful or
/// unsuccessful).
pub fn handle_form(request: &Request) -> Response {
// query the request body for form data
// return a 400 error if the admin_id field is missing
let data = try_or_400!(post_input!(request, {
id: String,
current_name: String,
current_description: String,
new_name: Option<String>,
new_description: Option<String>,
image: Option<BufferedFile>,
}));
let (flash_name, flash_msg) = match SbotStatus::read() {
Ok(status) if status.state == Some("active".to_string()) => {
// we can't pass `data` into the function (due to macro creation)
// so we pass in each individual value instead
match sbot::update_profile_info(
data.current_name,
data.current_description,
data.new_name,
data.new_description,
data.image,
) {
Ok(success_msg) => (
"flash_name=success".to_string(),
format!("flash_msg={}", success_msg),
),
Err(error_msg) => (
"flash_name=error".to_string(),
format!("flash_msg={}", error_msg),
),
}
}
_ => (
"flash_name=warning".to_string(),
"Profile is unavailable.".to_string(),
),
};
Response::redirect_303("/scuttlebutt/profile/update").add_flash(flash_name, flash_msg)
}
/*
match sbot::validate_public_key(&data.public_key) {
Ok(_) => {
let url = format!("/scuttlebutt/profile?={}", &data.public_key);
Response::redirect_303(url)
}
Err(err) => {
let (flash_name, flash_msg) =
("flash_name=error".to_string(), format!("flash_msg={}", err));
Response::redirect_303("/scuttlebutt/search").add_flash(flash_name, flash_msg)
}
}
}
*/

View File

@ -4,6 +4,7 @@ use std::{
fs,
fs::File,
io,
io::prelude::*,
path::Path,
process::{Command, Output},
};
@ -12,8 +13,8 @@ use async_std::task;
use dirs;
use futures::stream::TryStreamExt;
use golgi::{api::friends::RelationshipQuery, blobs, messages::SsbMessageValue, Sbot};
use log::info;
use peach_lib::sbot::SbotConfig;
use rouille::input::post::BufferedFile;
use temporary::Directory;
use crate::{error::PeachWebError, utils::sbot};
@ -168,13 +169,10 @@ pub fn get_profile_info(ssb_id: Option<String>) -> Result<Profile, Box<dyn Error
// if an ssb_id has been provided, we assume that the profile info
// being retrieved is for a peer (ie. not for our local profile)
let id = if ssb_id.is_some() {
let id = if let Some(peer_id) = ssb_id {
// we are not dealing with the local profile
profile.is_local_profile = false;
// we're safe to unwrap here because we know it's `Some(id)`
let peer_id = ssb_id.unwrap();
// determine relationship between peer and local id
let follow_query = RelationshipQuery {
source: local_id.clone(),
@ -228,7 +226,7 @@ pub fn get_profile_info(ssb_id: Option<String>) -> Result<Profile, Box<dyn Error
// determine the path to the blob defined by the value of `profile.image`
if let Some(ref blob_id) = profile.image {
profile.blob_path = match blobs::get_blob_path(&blob_id) {
profile.blob_path = match blobs::get_blob_path(blob_id) {
Ok(path) => {
// if we get the path, check if the blob is in the blobstore.
// this allows us to default to a placeholder image in the template
@ -246,6 +244,84 @@ pub fn get_profile_info(ssb_id: Option<String>) -> Result<Profile, Box<dyn Error
})
}
/// Update the profile info for the local public key.
///
/// Profile info includes name, description and image.
pub fn update_profile_info(
current_name: String,
current_description: String,
new_name: Option<String>,
new_description: Option<String>,
image: Option<BufferedFile>,
) -> Result<String, String> {
// retrieve latest go-sbot configuration parameters
let sbot_config = SbotConfig::read().ok();
task::block_on(async {
let mut sbot_client = init_sbot_with_config(&sbot_config)
.await
.map_err(|e| e.to_string())?;
// track whether the name, description or image have been updated
let mut name_updated: bool = false;
let mut description_updated: bool = false;
let mut image_updated: bool = false;
// check if a new_name value has been submitted in the form
if let Some(name) = new_name {
// only update the name if it has changed
if name != current_name {
// TODO: debug!("Publishing new Scuttlebutt profile name");
if let Err(e) = sbot_client.publish_name(&name).await {
return Err(format!("Failed to update name: {}", e));
} else {
name_updated = true
}
}
}
if let Some(description) = new_description {
// only update the description if it has changed
if description != current_description {
//debug!("Publishing new Scuttlebutt profile description");
if let Err(e) = sbot_client.publish_description(&description).await {
return Err(format!("Failed to update description: {}", e));
} else {
description_updated = true
}
}
}
// only update the image if a file was uploaded
if let Some(img) = image {
// only write the blob if it has a filename and data > 0 bytes
if img.filename.is_some() && !img.data.is_empty() {
match write_blob_to_store(img).await {
Ok(blob_id) => {
// if the file was successfully added to the blobstore,
// publish an about image message with the blob id
if let Err(e) = sbot_client.publish_image(&blob_id).await {
return Err(format!("Failed to update image: {}", e));
} else {
image_updated = true
}
}
Err(e) => return Err(format!("Failed to add image to blobstore: {}", e)),
}
} else {
image_updated = false
}
}
if name_updated || description_updated || image_updated {
Ok("Profile updated".to_string())
} else {
// no updates were made but no errors were encountered either
Ok("Profile info unchanged".to_string())
}
})
}
/// Retrieve a list of peers blocked by the local public key.
pub fn get_blocks_list() -> Result<Vec<HashMap<String, String>>, Box<dyn Error>> {
// retrieve latest go-sbot configuration parameters
@ -306,13 +382,13 @@ pub fn get_follows_list() -> Result<Vec<HashMap<String, String>>, Box<dyn Error>
// retrieve the profile image blob id for the given peer
if let Some(blob_id) = peer_info.get("image") {
// look-up the path for the image blob
if let Ok(blob_path) = blobs::get_blob_path(&blob_id) {
if let Ok(blob_path) = blobs::get_blob_path(blob_id) {
// insert the image blob path of the peer into the info hashmap
peer_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 blob_is_stored_locally(&blob_path).await {
Ok(exists) if exists == true => {
Ok(exists) if exists => {
peer_info.insert("blob_exists".to_string(), "true".to_string())
}
_ => peer_info.insert("blob_exists".to_string(), "false".to_string()),
@ -356,13 +432,13 @@ pub fn get_friends_list() -> Result<Vec<HashMap<String, String>>, Box<dyn Error>
// retrieve the profile image blob id for the given peer
if let Some(blob_id) = peer_info.get("image") {
// look-up the path for the image blob
if let Ok(blob_path) = blobs::get_blob_path(&blob_id) {
if let Ok(blob_path) = blobs::get_blob_path(blob_id) {
// insert the image blob path of the peer into the info hashmap
peer_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 sbot::blob_is_stored_locally(&blob_path).await {
Ok(exists) if exists == true => {
Ok(exists) if exists => {
peer_info.insert("blob_exists".to_string(), "true".to_string())
}
_ => peer_info.insert("blob_exists".to_string(), "false".to_string()),
@ -401,7 +477,7 @@ pub fn get_go_ssb_path() -> Result<String, PeachWebError> {
// return the default path if unable to read `config.toml`
Err(_) => {
// determine the home directory
let mut home_path = dirs::home_dir().ok_or_else(|| PeachWebError::HomeDir)?;
let mut home_path = dirs::home_dir().ok_or(PeachWebError::HomeDir)?;
// add the go-ssb subdirectory
home_path.push(".ssb-go");
// convert the PathBuf to a String
@ -422,32 +498,40 @@ pub async fn blob_is_stored_locally(blob_path: &str) -> Result<bool, PeachWebErr
Ok(blob_exists_locally)
}
/*
// take the path to a file, add it to the blobstore and return the blob id
pub async fn write_blob_to_store(file: &mut TempFile<'_>) -> Result<String, PeachWebError> {
pub async fn write_blob_to_store(image: BufferedFile) -> Result<String, PeachWebError> {
// we performed a `image.filename.is_some()` check before calling `write_blob_to_store`
// so it should be safe to do a simple unwrap here
let filename = image
.filename
.expect("retrieving filename from uploaded file");
// create temporary directory and path
let temp_dir = Directory::new("blob")?;
// we performed a `file.name().is_some()` check before calling `write_blob_to_store`
// so it should be safe to do a simple unwrap here
let filename = file.name().expect("retrieving filename from uploaded file");
let temp_path = temp_dir.join(filename);
// write file to temporary path
file.persist_to(&temp_path).await?;
fs::write(&temp_path, &image.data)?;
// open the file and read it into a buffer
let mut file = File::open(&temp_path)?;
let mut buffer = Vec::new();
file.read_to_end(&mut buffer)?;
// hash the bytes representing the file
let (hex_hash, blob_id) = blobs::hash_blob(&buffer)?;
// define the blobstore path and blob filename
let (blob_dir, blob_filename) = hex_hash.split_at(2);
let go_ssb_path = get_go_ssb_path()?;
let blobstore_sub_dir = format!("{}/blobs/sha256/{}", go_ssb_path, blob_dir);
// create the blobstore sub-directory
fs::create_dir_all(&blobstore_sub_dir)?;
// copy the file to the blobstore
let blob_path = format!("{}/{}", blobstore_sub_dir, blob_filename);
fs::copy(temp_path, blob_path)?;
Ok(blob_id)
}
*/