2025-03-08 16:24:26 -05:00

671 lines
23 KiB
Rust

use std::{
collections::HashMap,
error::Error,
fs,
fs::File,
io::prelude::*,
path::Path,
process::{Command, Output},
};
use async_std::task;
use dirs;
use futures::stream::TryStreamExt;
use log::debug;
use peach_lib::config_manager;
use peach_lib::sbot::SbotConfig;
use peach_lib::sbot::init_sbot;
use peach_lib::ssb_messages::SsbMessageKVT;
use rouille::input::post::BufferedFile;
use temporary::Directory;
use peach_lib::serde_json::json;
use peach_lib::tilde_client::TildeClient;
use crate::{error::PeachWebError, utils::sbot};
// SBOT HELPER FUNCTIONS
/// Executes a systemctl command for the solar-sbot.service process.
pub fn systemctl_sbot_cmd(cmd: &str) -> Result<Output, PeachWebError> {
let output = Command::new("sudo")
.arg("systemctl")
.arg(cmd)
.arg(config_manager::get_config_value("TILDE_SBOT_SERVICE")?)
.output()?;
Ok(output)
}
/// Executes a systemctl stop command followed by start command.
/// Returns a redirect with a flash message stating the output of the restart attempt.
pub fn restart_sbot_process() -> (String, String) {
debug!("Restarting solar-sbot.service");
match systemctl_sbot_cmd("stop") {
// if stop was successful, try to start the process
Ok(_) => match systemctl_sbot_cmd("start") {
Ok(_) => (
"success".to_string(),
"Updated configuration and restarted the sbot process".to_string(),
),
Err(err) => (
"error".to_string(),
format!(
"Updated configuration but failed to start the sbot process: {}",
err
),
),
},
Err(err) => (
"error".to_string(),
format!(
"Updated configuration but failed to stop the sbot process: {}",
err
),
),
}
}
/// Initialise an sbot client with the given configuration parameters.
pub async fn init_sbot_client() -> Result<TildeClient, PeachWebError> {
debug!("Initialising an sbot client with configuration parameters");
// initialise sbot connection with ip:port and shscap from config file
let key_path = format!(
"{}/secret.toml",
config_manager::get_config_value("TILDE_SBOT_DATADIR")?
);
let sbot_client = init_sbot().await?;
Ok(sbot_client)
}
// SCUTTLEBUTT FUNCTIONS
/// 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("Invalid key: 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("Invalid key: no dot index was found".to_string()),
};
// check hashing algorithm (must end with ".ed25519")
if !&public_key.ends_with(".ed25519") {
return Err("Invalid key: hashing algorithm must be ed25519".to_string());
}
// 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 Err("Invalid key: base64 data length is incorrect".to_string());
}
Ok(())
}
/// Calculate the latest sequence number for the local profile.
///
/// Retrieves a list of all messages authored by the local public key,
/// reverses the list and reads the sequence number of the most recently
/// authored message. This gives us the size of the database in terms of
/// the total number of locally-authored messages.
pub fn latest_sequence_number() -> Result<u64, PeachWebError> {
// retrieve latest solar-sbot configuration parameters
let sbot_config = SbotConfig::read().ok();
task::block_on(async {
let mut sbot_client = init_sbot_client().await?;
Err(PeachWebError::NotYetImplemented)
// retrieve the local id
// let id = sbot_client.whoami().await?;
// let history_stream = sbot_client.feed(&id).await?;
// let mut msgs: Vec<SsbMessageKVT> = history_stream.try_collect().await?;
//
// // there will be zero messages when the sbot is run for the first time
// if msgs.is_empty() {
// Ok(0)
// } else {
// // reverse the list of messages so we can easily reference the latest one
// msgs.reverse();
//
// // return the sequence number of the latest msg
// Ok(msgs[0].value.sequence)
// }
})
}
pub fn create_invite(uses: u16) -> Result<String, PeachWebError> {
// retrieve latest solar-sbot configuration parameters
let sbot_config = SbotConfig::read().ok();
task::block_on(async {
let mut sbot_client = init_sbot_client().await?;
debug!("Generating Scuttlebutt invite code");
let mut invite_code = sbot_client.create_invite(uses as i32).await?;
// // TODO: insert domain into invite if one is configured
// let domain = config_manager::get_config_value("EXTERNAL_DOMAIN")?;
// if !domain.is_empty() {
// invite_code = domain + &invite_code[4..];
// }
//
Ok(invite_code)
})
}
#[derive(Debug)]
pub struct Profile {
// is this the local profile or the profile of a peer?
pub is_local_profile: bool,
// an ssb_id which may or may not be the local public key
pub id: Option<String>,
pub name: Option<String>,
pub description: Option<String>,
pub image: Option<String>,
// the path to the blob defined in the `image` field (aka the profile picture)
pub blob_path: Option<String>,
// whether or not the blob exists in the blobstore (ie. is saved on disk)
pub blob_exists: bool,
// relationship state (if the profile being viewed is not for the local public key)
pub following: Option<bool>,
pub blocking: Option<bool>,
}
impl Profile {
pub fn default() -> Self {
Profile {
is_local_profile: true,
id: None,
name: None,
description: None,
image: None,
blob_path: None,
blob_exists: false,
following: None,
blocking: None,
}
}
}
/// Retrieve the profile info for the given public key.
pub fn get_profile_info(ssb_id: Option<String>) -> Result<Profile, Box<dyn Error>> {
task::block_on(async {
let sbot_client = init_sbot_client().await?;
let local_id = sbot_client.whoami().await?;
let mut profile = Profile::default();
// 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 let Some(peer_id) = ssb_id {
// we are not dealing with the local profile
profile.is_local_profile = false;
// query follow state
profile.following = Some(sbot_client.is_following(&local_id, &peer_id).await?);
// TODO: implement this check in solar_client so that this can be a real value
profile.blocking = Some(false);
peer_id
} else {
// if an ssb_id has not been provided, retrieve the local id using whoami
profile.is_local_profile = true;
local_id
};
// retrieve the profile info for the given id
let info = get_peer_info(&id).await?;
// set each profile field accordingly
for (key, val) in info {
match key.as_str() {
"name" => profile.name = Some(val),
"description" => profile.description = Some(val),
"image" => profile.image = Some(val),
_ => (),
}
}
//
// assign the ssb public key
// (could be for the local profile or a peer)
profile.id = Some(id);
// TODO: blobs support
// // 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) {
// 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
// if let Ok(exists) = blob_is_stored_locally(&path).await {
// profile.blob_exists = exists
// };
//
// Some(path)
// }
// Err(_) => None,
// }
// }
Ok(profile)
})
}
/// 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, PeachWebError> {
// retrieve latest solar-sbot configuration parameters
let sbot_config = SbotConfig::read().ok();
task::block_on(async {
let mut sbot_client = init_sbot_client()
.await?;
Err(PeachWebError::NotYetImplemented)
// // 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 {
// debug!("Publishing a 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 a 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())
// }
})
}
/// Follow a peer.
pub fn follow_peer(public_key: &str) -> Result<String, PeachWebError> {
// retrieve latest solar-sbot configuration parameters
let sbot_config = SbotConfig::read().ok();
task::block_on(async {
let mut sbot_client = init_sbot_client()
.await?;
debug!("Following a Scuttlebutt peer");
Err(PeachWebError::NotYetImplemented)
})
}
/// Unfollow a peer.
pub fn unfollow_peer(public_key: &str) -> Result<String, PeachWebError> {
// retrieve latest solar-sbot configuration parameters
let sbot_config = SbotConfig::read().ok();
task::block_on(async {
let mut sbot_client = init_sbot_client()
.await?;
Err(PeachWebError::NotYetImplemented)
// debug!("Unfollowing a Scuttlebutt peer");
// match sbot_client.unfollow(public_key).await {
// Ok(_) => Ok("Unfollowed peer".to_string()),
// Err(e) => Err(format!("Failed to unfollow peer: {}", e)),
// }
})
}
/// Block a peer.
pub fn block_peer(public_key: &str) -> Result<String, String> {
// retrieve latest solar-sbot configuration parameters
let sbot_config = SbotConfig::read().ok();
task::block_on(async {
let mut sbot_client = init_sbot_client()
.await
.map_err(|e| e.to_string())?;
debug!("Blocking a Scuttlebutt peer");
match sbot_client.create_block(public_key).await {
Ok(_) => Ok("Blocked peer".to_string()),
Err(e) => Err(format!("Failed to block peer: {}", e)),
}
})
}
/// Unblock a peer.
pub fn unblock_peer(public_key: &str) -> Result<String, PeachWebError> {
// retrieve latest solar-sbot configuration parameters
let sbot_config = SbotConfig::read().ok();
task::block_on(async {
let mut sbot_client = init_sbot_client()
.await?;
debug!("Unblocking a Scuttlebutt peer");
Err(PeachWebError::NotYetImplemented)
// match sbot_client.unblock(public_key).await {
// Ok(_) => Ok("Unblocked peer".to_string()),
// Err(e) => Err(format!("Failed to unblock peer: {}", e)),
// }
})
}
/// Retrieve a list of peers blocked by the local public key.
pub fn get_blocks_list() -> Result<Vec<HashMap<String, String>>, Box<dyn Error>> {
// populate this vec to return
let mut to_return: Vec<HashMap<String, String>> = Vec::new();
task::block_on(async {
let mut sbot_client = init_sbot_client().await?;
let self_id = sbot_client.whoami().await?;
let blocks = sbot_client.get_blocks(&self_id).await?;
if !blocks.is_empty() {
for peer in blocks.iter() {
// trim whitespace (including newline characters) and
// remove the inverted-commas around the id
// TODO: is this necessary?
let key = peer.trim().replace('"', "");
let peer_info = get_peer_info(&key).await?;
// push profile info to peer_list vec
to_return.push(peer_info)
}
}
// return the list of peers
Ok(to_return)
})
}
pub async fn get_peer_info(key: &str) -> Result<HashMap<String, String>, Box<dyn Error>> {
let mut sbot_client = init_sbot_client().await?;
// key,value dict of info about this peer
let tilde_profile_info = sbot_client.get_profile_info(key).await.map_err(|err| {
println!("error getting profile info: {}", err);
err
}
)?;
let mut peer_info = HashMap::new();
tilde_profile_info.get("name").and_then(|val| val.as_str()).map(|val| {
peer_info.insert("name".to_string(), val.to_string());
});
tilde_profile_info.get("description").and_then(|val| val.as_str()).map(|val| {
peer_info.insert("description".to_string(), val.to_string());
});
// insert the public key of the peer into the info hashmap
peer_info.insert("id".to_string(), key.to_string());
// retrieve the profile image blob id for the given peer
// TODO: blob support
// 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) {
// // 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 => {
// peer_info.insert("blob_exists".to_string(), "true".to_string())
// }
// _ => peer_info.insert("blob_exists".to_string(), "false".to_string()),
// };
// }
// }
Ok(peer_info)
}
/// Retrieve a list of peers followed by the local public key.
pub fn get_follows_list() -> Result<Vec<HashMap<String, String>>, Box<dyn Error>> {
// populate this vec to return
let mut to_return: Vec<HashMap<String, String>> = Vec::new();
task::block_on(async {
let mut sbot_client = init_sbot_client().await?;
let self_id = sbot_client.whoami().await?;
let follows = sbot_client.get_follows(&self_id).await?;
if !follows.is_empty() {
for peer in follows.iter() {
// trim whitespace (including newline characters) and
// remove the inverted-commas around the id
// TODO: is this necessary?
let key = peer.trim().replace('"', "");
let peer_info = get_peer_info(&key).await?;
// push profile info to peer_list vec
to_return.push(peer_info)
}
}
// return the list of peers
Ok(to_return)
})
}
/// Retrieve a list of peers friended by the local public key.
pub fn get_friends_list() -> Result<Vec<HashMap<String, String>>, Box<dyn Error>> {
// populate this vec to return
let mut to_return: Vec<HashMap<String, String>> = Vec::new();
task::block_on(async {
let mut sbot_client = init_sbot_client().await?;
let self_id = sbot_client.whoami().await?;
let friends = sbot_client.get_friends(&self_id).await?;
if !friends.is_empty() {
for peer in friends.iter() {
// trim whitespace (including newline characters) and
// remove the inverted-commas around the id
// TODO: is this necessary?
let key = peer.trim().replace('"', "");
let peer_info = get_peer_info(&key).await?;
// push profile info to peer_list vec
to_return.push(peer_info)
}
}
// return the list of peers
Ok(to_return)
})
}
/// Retrieve the local public key (id).
pub fn get_local_id() -> Result<String, Box<dyn Error>> {
// retrieve latest solar-sbot configuration parameters
let sbot_config = SbotConfig::read().ok();
task::block_on(async {
let mut sbot_client = init_sbot_client().await?;
let local_id = sbot_client.whoami().await?;
Ok(local_id)
})
}
/// Publish a public post.
pub fn publish_public_post(text: String) -> Result<String, String> {
// retrieve latest solar-sbot configuration parameters
let sbot_config = SbotConfig::read().ok();
task::block_on(async {
let mut sbot_client = init_sbot_client()
.await
.map_err(|e| e.to_string())?;
debug!("Publishing a new Scuttlebutt public post");
let post = json!({
"type": "post",
"text": &text,
});
match sbot_client.publish(post).await {
Ok(_) => Ok("Published post".to_string()),
Err(e) => Err(format!("Failed to publish post: {}", e)),
}
})
}
/// Publish a private message.
pub fn publish_private_msg(text: String, recipients: Vec<String>) -> Result<String, PeachWebError> {
// retrieve latest solar-sbot configuration parameters
let sbot_config = SbotConfig::read().ok();
task::block_on(async {
let mut sbot_client = init_sbot_client()
.await?;
Err(PeachWebError::NotYetImplemented)
// debug!("Publishing a new Scuttlebutt private message");
// match sbot_client
// .publish_private(text.to_string(), recipients)
// .await
// {
// Ok(_) => Ok("Published private message".to_string()),
// Err(e) => Err(format!("Failed to publish private message: {}", e)),
// }
})
}
// FILEPATH FUNCTIONS
/// Return the path of the ssb-go directory.
pub fn get_go_ssb_path() -> Result<String, PeachWebError> {
let go_ssb_path = match SbotConfig::read() {
Ok(conf) => conf.repo,
// return the default path if unable to read `config.toml`
Err(_) => {
// determine the home directory
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
home_path
.into_os_string()
.into_string()
.map_err(|_| PeachWebError::OsString)?
}
};
Ok(go_ssb_path)
}
/// Check whether a blob is in the blobstore.
pub async fn blob_is_stored_locally(blob_path: &str) -> Result<bool, PeachWebError> {
let go_ssb_path = get_go_ssb_path()?;
let complete_path = format!("{}/blobs/sha256/{}", go_ssb_path, blob_path);
let blob_exists_locally = Path::new(&complete_path).exists();
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(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")?;
let temp_path = temp_dir.join(filename);
// write file to temporary path
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)?;
Err(PeachWebError::NotYetImplemented)
// TODO: not yet implemented
// // 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)
}