2022-03-20 14:38:32 +00:00
|
|
|
use std::{
|
|
|
|
collections::HashMap,
|
|
|
|
error::Error,
|
|
|
|
fs,
|
|
|
|
fs::File,
|
2022-03-22 14:11:52 +00:00
|
|
|
io::prelude::*,
|
2022-03-20 14:38:32 +00:00
|
|
|
path::Path,
|
|
|
|
process::{Command, Output},
|
|
|
|
};
|
2022-03-20 10:26:04 +00:00
|
|
|
|
|
|
|
use async_std::task;
|
|
|
|
use dirs;
|
|
|
|
use futures::stream::TryStreamExt;
|
2022-03-20 14:38:32 +00:00
|
|
|
use golgi::{api::friends::RelationshipQuery, blobs, messages::SsbMessageValue, Sbot};
|
2022-03-24 07:27:06 +00:00
|
|
|
use log::debug;
|
2022-05-26 21:19:31 +00:00
|
|
|
use peach_lib::config_manager::get_config_value;
|
2022-03-20 10:26:04 +00:00
|
|
|
use peach_lib::sbot::SbotConfig;
|
2022-03-22 14:11:52 +00:00
|
|
|
use rouille::input::post::BufferedFile;
|
2022-03-20 10:26:04 +00:00
|
|
|
use temporary::Directory;
|
|
|
|
|
2022-03-20 14:38:32 +00:00
|
|
|
use crate::{error::PeachWebError, utils::sbot};
|
2022-03-20 10:26:04 +00:00
|
|
|
|
|
|
|
// SBOT HELPER FUNCTIONS
|
|
|
|
|
2022-03-20 14:38:32 +00:00
|
|
|
/// Executes a systemctl command for the go-sbot.service process.
|
2022-05-26 21:17:20 +00:00
|
|
|
pub fn systemctl_sbot_cmd(cmd: &str) -> Result<Output, PeachWebError> {
|
|
|
|
let output = Command::new("sudo")
|
2022-04-14 18:47:43 +00:00
|
|
|
.arg("systemctl")
|
2022-03-20 14:38:32 +00:00
|
|
|
.arg(cmd)
|
2022-05-26 21:17:20 +00:00
|
|
|
.arg(get_config_value("GO_SBOT_SERVICE")?)
|
|
|
|
.output()?;
|
|
|
|
Ok(output)
|
2022-03-20 14:38:32 +00:00
|
|
|
}
|
|
|
|
|
2022-03-23 07:14:54 +00:00
|
|
|
/// 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) {
|
2022-03-24 07:27:06 +00:00
|
|
|
debug!("Restarting go-sbot.service");
|
2022-03-23 07:14:54 +00:00
|
|
|
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
|
|
|
|
),
|
|
|
|
),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-03-20 14:38:32 +00:00
|
|
|
/// Initialise an sbot client with the given configuration parameters.
|
2022-03-20 10:26:04 +00:00
|
|
|
pub async fn init_sbot_with_config(
|
|
|
|
sbot_config: &Option<SbotConfig>,
|
|
|
|
) -> Result<Sbot, PeachWebError> {
|
2022-03-24 07:27:06 +00:00
|
|
|
debug!("Initialising an sbot client with configuration parameters");
|
2022-03-20 10:26:04 +00:00
|
|
|
// initialise sbot connection with ip:port and shscap from config file
|
|
|
|
let sbot_client = match sbot_config {
|
|
|
|
// TODO: panics if we pass `Some(conf.shscap)` as second arg
|
|
|
|
Some(conf) => {
|
|
|
|
let ip_port = conf.lis.clone();
|
|
|
|
Sbot::init(Some(ip_port), None).await?
|
|
|
|
}
|
|
|
|
None => Sbot::init(None, None).await?,
|
|
|
|
};
|
|
|
|
|
|
|
|
Ok(sbot_client)
|
|
|
|
}
|
|
|
|
|
2022-03-21 11:27:32 +00:00
|
|
|
// 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()),
|
2022-03-20 10:26:04 +00:00
|
|
|
};
|
|
|
|
|
2022-03-21 11:27:32 +00:00
|
|
|
// check hashing algorithm (must end with ".ed25519")
|
|
|
|
if !&public_key.ends_with(".ed25519") {
|
|
|
|
return Err("Invalid key: hashing algorithm must be ed25519".to_string());
|
|
|
|
}
|
2022-03-20 10:26:04 +00:00
|
|
|
|
2022-03-21 11:27:32 +00:00
|
|
|
// 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(())
|
2022-03-20 10:26:04 +00:00
|
|
|
}
|
|
|
|
|
2022-03-21 11:27:32 +00:00
|
|
|
/// 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.
|
2022-03-20 10:26:04 +00:00
|
|
|
pub fn latest_sequence_number() -> Result<u64, Box<dyn Error>> {
|
|
|
|
// 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?;
|
|
|
|
|
|
|
|
// retrieve the local id
|
|
|
|
let id = sbot_client.whoami().await?;
|
|
|
|
|
|
|
|
let history_stream = sbot_client.create_history_stream(id).await?;
|
|
|
|
let mut msgs: Vec<SsbMessageValue> = history_stream.try_collect().await?;
|
|
|
|
|
2022-03-24 11:41:49 +00:00
|
|
|
// 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();
|
2022-03-20 10:26:04 +00:00
|
|
|
|
2022-03-24 11:41:49 +00:00
|
|
|
// return the sequence number of the latest msg
|
|
|
|
Ok(msgs[0].sequence)
|
|
|
|
}
|
2022-03-20 10:26:04 +00:00
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2022-03-21 11:27:32 +00:00
|
|
|
pub fn create_invite(uses: u16) -> Result<String, Box<dyn Error>> {
|
|
|
|
// 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?;
|
|
|
|
|
2022-03-24 07:27:06 +00:00
|
|
|
debug!("Generating Scuttlebutt invite code");
|
2022-03-21 11:27:32 +00:00
|
|
|
let invite_code = sbot_client.invite_create(uses).await?;
|
|
|
|
|
|
|
|
Ok(invite_code)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2022-03-21 14:40:54 +00:00
|
|
|
#[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>> {
|
|
|
|
// 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?;
|
|
|
|
|
|
|
|
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)
|
2022-03-22 14:11:52 +00:00
|
|
|
let id = if let Some(peer_id) = ssb_id {
|
2022-03-21 14:40:54 +00:00
|
|
|
// we are not dealing with the local profile
|
|
|
|
profile.is_local_profile = false;
|
|
|
|
|
|
|
|
// determine relationship between peer and local id
|
|
|
|
let follow_query = RelationshipQuery {
|
|
|
|
source: local_id.clone(),
|
|
|
|
dest: peer_id.clone(),
|
|
|
|
};
|
|
|
|
|
|
|
|
// query follow state
|
|
|
|
profile.following = match sbot_client.friends_is_following(follow_query).await {
|
|
|
|
Ok(following) if following == "true" => Some(true),
|
|
|
|
Ok(following) if following == "false" => Some(false),
|
|
|
|
_ => None,
|
|
|
|
};
|
|
|
|
|
|
|
|
// TODO: i don't like that we have to instantiate the same query object
|
|
|
|
// twice. see if we can streamline this in golgi
|
|
|
|
let block_query = RelationshipQuery {
|
|
|
|
source: local_id.clone(),
|
|
|
|
dest: peer_id.clone(),
|
|
|
|
};
|
|
|
|
|
|
|
|
// query block state
|
|
|
|
profile.blocking = match sbot_client.friends_is_blocking(block_query).await {
|
|
|
|
Ok(blocking) if blocking == "true" => Some(true),
|
|
|
|
Ok(blocking) if blocking == "false" => Some(false),
|
|
|
|
_ => None,
|
|
|
|
};
|
|
|
|
|
|
|
|
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 = sbot_client.get_profile_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);
|
|
|
|
|
|
|
|
// determine the path to the blob defined by the value of `profile.image`
|
|
|
|
if let Some(ref blob_id) = profile.image {
|
2022-03-22 14:11:52 +00:00
|
|
|
profile.blob_path = match blobs::get_blob_path(blob_id) {
|
2022-03-21 14:40:54 +00:00
|
|
|
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)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2022-03-22 14:11:52 +00:00
|
|
|
/// 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 {
|
2022-03-24 07:27:06 +00:00
|
|
|
debug!("Publishing a new Scuttlebutt profile name");
|
2022-03-22 14:11:52 +00:00
|
|
|
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 {
|
2022-03-24 07:27:06 +00:00
|
|
|
debug!("Publishing a new Scuttlebutt profile description");
|
2022-03-22 14:11:52 +00:00
|
|
|
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())
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2022-03-24 11:41:49 +00:00
|
|
|
/// Follow a peer.
|
|
|
|
pub fn follow_peer(public_key: &str) -> 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())?;
|
|
|
|
|
|
|
|
debug!("Following a Scuttlebutt peer");
|
|
|
|
match sbot_client.follow(public_key).await {
|
|
|
|
Ok(_) => Ok("Followed peer".to_string()),
|
|
|
|
Err(e) => Err(format!("Failed to follow peer: {}", e)),
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Unfollow a peer.
|
|
|
|
pub fn unfollow_peer(public_key: &str) -> 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())?;
|
|
|
|
|
|
|
|
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 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())?;
|
|
|
|
|
|
|
|
debug!("Blocking a Scuttlebutt peer");
|
|
|
|
match sbot_client.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, 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())?;
|
|
|
|
|
|
|
|
debug!("Unblocking a Scuttlebutt peer");
|
|
|
|
match sbot_client.unblock(public_key).await {
|
|
|
|
Ok(_) => Ok("Unblocked peer".to_string()),
|
|
|
|
Err(e) => Err(format!("Failed to unblock peer: {}", e)),
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2022-03-21 11:27:32 +00:00
|
|
|
/// Retrieve a list of peers blocked by the local public key.
|
2022-03-20 10:26:04 +00:00
|
|
|
pub fn get_blocks_list() -> Result<Vec<HashMap<String, String>>, Box<dyn Error>> {
|
|
|
|
// 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?;
|
|
|
|
|
|
|
|
let blocks = sbot_client.get_blocks().await?;
|
|
|
|
|
|
|
|
// we'll use this to store the profile info for each peer whom we block
|
|
|
|
let mut peer_list = 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 peer_info = sbot_client.get_profile_info(&key).await?;
|
|
|
|
// insert the public key of the peer into the info hashmap
|
|
|
|
peer_info.insert("id".to_string(), key.to_string());
|
|
|
|
// we do not even attempt to find the blob for a blocked peer,
|
|
|
|
// since it may be vulgar to cause distress to the local peer.
|
|
|
|
peer_info.insert("blob_exists".to_string(), "false".to_string());
|
|
|
|
// push profile info to peer_list vec
|
|
|
|
peer_list.push(peer_info)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// return the list of blocked peers
|
|
|
|
Ok(peer_list)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2022-03-21 11:27:32 +00:00
|
|
|
/// Retrieve a list of peers followed by the local public key.
|
2022-03-20 10:26:04 +00:00
|
|
|
pub fn get_follows_list() -> Result<Vec<HashMap<String, String>>, Box<dyn Error>> {
|
|
|
|
// 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?;
|
|
|
|
|
|
|
|
let follows = sbot_client.get_follows().await?;
|
|
|
|
|
|
|
|
// we'll use this to store the profile info for each peer who follows us
|
|
|
|
let mut peer_list = Vec::new();
|
|
|
|
|
|
|
|
if !follows.is_empty() {
|
|
|
|
for peer in follows.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 peer_info = sbot_client.get_profile_info(&key).await?;
|
|
|
|
// 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
|
|
|
|
if let Some(blob_id) = peer_info.get("image") {
|
|
|
|
// look-up the path for the image blob
|
2022-03-22 14:11:52 +00:00
|
|
|
if let Ok(blob_path) = blobs::get_blob_path(blob_id) {
|
2022-03-20 10:26:04 +00:00
|
|
|
// 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 {
|
2022-03-22 14:11:52 +00:00
|
|
|
Ok(exists) if exists => {
|
2022-03-20 10:26:04 +00:00
|
|
|
peer_info.insert("blob_exists".to_string(), "true".to_string())
|
|
|
|
}
|
|
|
|
_ => peer_info.insert("blob_exists".to_string(), "false".to_string()),
|
|
|
|
};
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// push profile info to peer_list vec
|
|
|
|
peer_list.push(peer_info)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// return the list of peers
|
|
|
|
Ok(peer_list)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2022-03-21 11:27:32 +00:00
|
|
|
/// Retrieve a list of peers friended by the local public key.
|
2022-03-20 14:38:32 +00:00
|
|
|
pub fn get_friends_list() -> Result<Vec<HashMap<String, String>>, Box<dyn Error>> {
|
2022-03-20 10:26:04 +00:00
|
|
|
// 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?;
|
|
|
|
|
2022-03-20 14:38:32 +00:00
|
|
|
let local_id = sbot_client.whoami().await?;
|
|
|
|
|
|
|
|
let follows = sbot_client.get_follows().await?;
|
|
|
|
|
|
|
|
// we'll use this to store the profile info for each friend
|
|
|
|
let mut peer_list = Vec::new();
|
|
|
|
|
|
|
|
if !follows.is_empty() {
|
|
|
|
for peer in follows.iter() {
|
|
|
|
// trim whitespace (including newline characters) and
|
|
|
|
// remove the inverted-commas around the id
|
|
|
|
let peer_id = peer.trim().replace('"', "");
|
|
|
|
// retrieve the profile info for the given peer
|
|
|
|
let mut peer_info = sbot_client.get_profile_info(&peer_id).await?;
|
|
|
|
// insert the public key of the peer into the info hashmap
|
|
|
|
peer_info.insert("id".to_string(), peer_id.to_string());
|
|
|
|
// 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
|
2022-03-22 14:11:52 +00:00
|
|
|
if let Ok(blob_path) = blobs::get_blob_path(blob_id) {
|
2022-03-20 14:38:32 +00:00
|
|
|
// 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 {
|
2022-03-22 14:11:52 +00:00
|
|
|
Ok(exists) if exists => {
|
2022-03-20 14:38:32 +00:00
|
|
|
peer_info.insert("blob_exists".to_string(), "true".to_string())
|
|
|
|
}
|
|
|
|
_ => peer_info.insert("blob_exists".to_string(), "false".to_string()),
|
|
|
|
};
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// check if the peer follows us (making us friends)
|
|
|
|
let follow_query = RelationshipQuery {
|
|
|
|
source: peer_id.to_string(),
|
|
|
|
dest: local_id.clone(),
|
|
|
|
};
|
2022-03-20 10:26:04 +00:00
|
|
|
|
2022-03-20 14:38:32 +00:00
|
|
|
// query follow state
|
|
|
|
match sbot_client.friends_is_following(follow_query).await {
|
|
|
|
Ok(following) if following == "true" => {
|
|
|
|
// only push profile info to peer_list vec if they follow us
|
|
|
|
peer_list.push(peer_info)
|
|
|
|
}
|
|
|
|
_ => (),
|
|
|
|
};
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// return the list of peers
|
|
|
|
Ok(peer_list)
|
2022-03-20 10:26:04 +00:00
|
|
|
})
|
|
|
|
}
|
2022-03-21 11:27:32 +00:00
|
|
|
|
2022-03-23 09:41:21 +00:00
|
|
|
/// Retrieve the local public key (id).
|
|
|
|
pub fn get_local_id() -> Result<String, Box<dyn Error>> {
|
|
|
|
// 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?;
|
|
|
|
|
|
|
|
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 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())?;
|
|
|
|
|
2022-03-24 07:27:06 +00:00
|
|
|
debug!("Publishing a new Scuttlebutt public post");
|
2022-03-23 09:41:21 +00:00
|
|
|
match sbot_client.publish_post(&text).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, 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())?;
|
|
|
|
|
2022-03-24 07:27:06 +00:00
|
|
|
debug!("Publishing a new Scuttlebutt private message");
|
2022-03-23 09:41:21 +00:00
|
|
|
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)),
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2022-03-21 11:27:32 +00:00
|
|
|
// 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
|
2022-03-22 14:11:52 +00:00
|
|
|
let mut home_path = dirs::home_dir().ok_or(PeachWebError::HomeDir)?;
|
2022-03-21 11:27:32 +00:00
|
|
|
// 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
|
2022-03-22 14:11:52 +00:00
|
|
|
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");
|
|
|
|
|
2022-03-21 11:27:32 +00:00
|
|
|
// create temporary directory and path
|
|
|
|
let temp_dir = Directory::new("blob")?;
|
|
|
|
let temp_path = temp_dir.join(filename);
|
2022-03-22 14:11:52 +00:00
|
|
|
|
2022-03-21 11:27:32 +00:00
|
|
|
// write file to temporary path
|
2022-03-22 14:11:52 +00:00
|
|
|
fs::write(&temp_path, &image.data)?;
|
|
|
|
|
2022-03-21 11:27:32 +00:00
|
|
|
// 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)?;
|
2022-03-22 14:11:52 +00:00
|
|
|
|
2022-03-21 11:27:32 +00:00
|
|
|
// hash the bytes representing the file
|
|
|
|
let (hex_hash, blob_id) = blobs::hash_blob(&buffer)?;
|
2022-03-22 14:11:52 +00:00
|
|
|
|
2022-03-21 11:27:32 +00:00
|
|
|
// 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);
|
2022-03-22 14:11:52 +00:00
|
|
|
|
2022-03-21 11:27:32 +00:00
|
|
|
// create the blobstore sub-directory
|
|
|
|
fs::create_dir_all(&blobstore_sub_dir)?;
|
2022-03-22 14:11:52 +00:00
|
|
|
|
2022-03-21 11:27:32 +00:00
|
|
|
// copy the file to the blobstore
|
|
|
|
let blob_path = format!("{}/{}", blobstore_sub_dir, blob_filename);
|
|
|
|
fs::copy(temp_path, blob_path)?;
|
2022-03-22 14:11:52 +00:00
|
|
|
|
2022-03-21 11:27:32 +00:00
|
|
|
Ok(blob_id)
|
|
|
|
}
|