Merge pull request 'Integrate golgi into peach_web' (#81) from golgi_integration into main

Reviewed-on: #81
This commit is contained in:
glyph 2022-03-03 07:37:23 +00:00
commit 10049f0bc6
23 changed files with 2252 additions and 514 deletions

644
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -35,7 +35,11 @@ travis-ci = { repository = "peachcloud/peach-web", branch = "master" }
maintenance = { status = "actively-developed" }
[dependencies]
base64 = "0.13.0"
dirs = "4.0.0"
env_logger = "0.8"
#golgi = "0.1.0"
golgi = { path = "../../../playground/rust/golgi" }
lazy_static = "1.4.0"
log = "0.4"
nest = "1.0.0"
@ -45,6 +49,7 @@ peach-stats = { path = "../peach-stats", features = ["serde_support"] }
rocket = { version = "0.5.0-rc.1", features = ["json", "secrets"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
temporary = "0.6.4"
tera = { version = "1.12.1", features = ["builtins"] }
xdg = "2.2.0"

View File

@ -1,2 +1,3 @@
pub mod dns;
pub mod network;
pub mod scuttlebutt;

View File

@ -0,0 +1,588 @@
use std::collections::HashMap;
use golgi::{api::friends::RelationshipQuery, blobs, messages::SsbMessageValue, Sbot};
use peach_lib::sbot::{SbotConfig, SbotStatus};
use rocket::{futures::TryStreamExt, serde::Serialize};
use crate::{error::PeachWebError, utils};
// HELPER FUNCTIONS
pub async fn init_sbot_with_config(
sbot_config: &Option<SbotConfig>,
) -> Result<Sbot, PeachWebError> {
// 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)
}
// CONTEXT STRUCTS AND BUILDERS
#[derive(Debug, Serialize)]
pub struct StatusContext {
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>,
// latest sequence number for the local log
pub latest_seq: Option<u64>,
}
impl StatusContext {
pub fn default() -> Self {
StatusContext {
back: Some("/".to_string()),
flash_name: None,
flash_msg: None,
title: Some("Scuttlebutt Status".to_string()),
theme: None,
sbot_config: None,
sbot_status: None,
latest_seq: 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?;
// 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?;
// reverse the list of messages so we can easily reference the latest one
msgs.reverse();
// assign the sequence number of the latest msg
context.latest_seq = Some(msgs[0].sequence);
context.sbot_config = sbot_config;
} 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, status 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 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());
// 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.
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>,
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 FollowsContext {
pub fn default() -> Self {
FollowsContext {
back: Some("/scuttlebutt/peers".to_string()),
flash_name: None,
flash_msg: None,
title: Some("Follows".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 follows = sbot_client.get_follows().await?;
// we'll use this to store the profile info for each peer who follows us
let mut peer_info = 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 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 follow and are followed by the local account (friends)
#[derive(Debug, Serialize)]
pub struct FriendsContext {
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 FriendsContext {
pub fn default() -> Self {
FriendsContext {
back: Some("/scuttlebutt/peers".to_string()),
flash_name: None,
flash_msg: None,
title: Some("Friends".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 local_id = sbot_client.whoami().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_info = 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 info = sbot_client.get_profile_info(&peer_id).await?;
// insert the public key of the peer into the info hashmap
info.insert("id".to_string(), peer_id.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()),
};
}
}
// check if the peer follows us (making us friends)
let follow_query = RelationshipQuery {
source: peer_id.to_string(),
dest: local_id.clone(),
};
// 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_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)
}
}
#[derive(Debug, Serialize)]
pub struct ProfileContext {
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>,
// 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 ProfileContext {
pub fn default() -> Self {
ProfileContext {
back: Some("/".to_string()),
flash_name: None,
flash_msg: None,
title: Some("Profile".to_string()),
theme: None,
sbot_config: None,
sbot_status: None,
is_local_profile: true,
id: None,
name: None,
description: None,
image: None,
blob_path: None,
blob_exists: false,
following: None,
blocking: None,
}
}
pub async fn build(ssb_id: Option<String>) -> 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 local_id = sbot_client.whoami().await?;
// if an ssb_id has been provided to the context builder, 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() {
// we are not dealing with the local profile
context.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(),
dest: peer_id.clone(),
};
// query follow state
context.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
context.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
context.is_local_profile = true;
local_id
};
// TODO: add relationship state context if not local profile
// ie. lookup is_following and is_blocking, set context accordingly
// retrieve the profile info for the given id
let info = sbot_client.get_profile_info(&id).await?;
// set each context field accordingly
for (key, val) in info {
match key.as_str() {
"name" => context.name = Some(val),
"description" => context.description = Some(val),
"image" => context.image = Some(val),
_ => (),
}
}
// assign the ssb public key to the context
// (could be for the local profile or a peer)
context.id = Some(id);
// determine the path to the blob defined by the value of `context.image`
if let Some(ref blob_id) = context.image {
context.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) = utils::blob_is_stored_locally(&path).await {
context.blob_exists = exists
};
Some(path)
}
Err(_) => None,
}
}
} 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.sbot_status = Some(sbot_status);
Ok(context)
}
}
#[derive(Debug, Serialize)]
pub struct PrivateContext {
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>,
// local peer id (whoami)
pub id: Option<String>,
// id of the peer being messaged
pub recipient_id: Option<String>,
}
impl PrivateContext {
pub fn default() -> Self {
PrivateContext {
back: Some("/".to_string()),
flash_name: None,
flash_msg: None,
title: Some("Private Messages".to_string()),
theme: None,
sbot_config: None,
sbot_status: None,
id: None,
recipient_id: None,
}
}
pub async fn build(recipient_id: Option<String>) -> 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?;
context.recipient_id = recipient_id;
let local_id = sbot_client.whoami().await?;
context.id = Some(local_id);
} 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, private messages cannot be published. Visit the Scuttlebutt settings menu to start the Sbot and then try again".to_string());
}
context.sbot_status = Some(sbot_status);
Ok(context)
}
}

View File

@ -1,5 +1,8 @@
//! Custom error type representing all possible error variants for peach-web.
use std::io::Error as IoError;
use golgi::GolgiError;
use peach_lib::error::PeachError;
use peach_lib::{serde_json, serde_yaml};
use serde_json::error::Error as JsonError;
@ -8,19 +11,27 @@ use serde_yaml::Error as YamlError;
/// Custom error type encapsulating all possible errors for the web application.
#[derive(Debug)]
pub enum PeachWebError {
Json(JsonError),
Yaml(YamlError),
FailedToRegisterDynDomain(String),
Golgi(GolgiError),
HomeDir,
Io(IoError),
Json(JsonError),
OsString,
PeachLib { source: PeachError, msg: String },
Yaml(YamlError),
}
impl std::error::Error for PeachWebError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match *self {
PeachWebError::Json(ref source) => Some(source),
PeachWebError::Yaml(ref source) => Some(source),
PeachWebError::FailedToRegisterDynDomain(_) => None,
PeachWebError::Golgi(ref source) => Some(source),
PeachWebError::HomeDir => None,
PeachWebError::Io(ref source) => Some(source),
PeachWebError::Json(ref source) => Some(source),
PeachWebError::OsString => None,
PeachWebError::PeachLib { ref source, .. } => Some(source),
PeachWebError::Yaml(ref source) => Some(source),
}
}
}
@ -28,28 +39,44 @@ impl std::error::Error for PeachWebError {
impl std::fmt::Display for PeachWebError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match *self {
PeachWebError::Json(ref source) => write!(f, "Serde JSON error: {}", source),
PeachWebError::Yaml(ref source) => write!(f, "Serde YAML error: {}", source),
PeachWebError::FailedToRegisterDynDomain(ref msg) => {
write!(f, "DYN DNS error: {}", msg)
}
PeachWebError::Golgi(ref source) => write!(f, "Golgi error: {}", source),
PeachWebError::HomeDir => write!(
f,
"Filesystem error: failed to determine home directory path"
),
PeachWebError::Io(ref source) => write!(f, "IO error: {}", source),
PeachWebError::Json(ref source) => write!(f, "Serde JSON error: {}", source),
PeachWebError::OsString => write!(
f,
"Filesystem error: failed to convert OsString to String for go-ssb directory path"
),
PeachWebError::PeachLib { ref source, .. } => write!(f, "{}", source),
PeachWebError::Yaml(ref source) => write!(f, "Serde YAML error: {}", source),
}
}
}
impl From<GolgiError> for PeachWebError {
fn from(err: GolgiError) -> PeachWebError {
PeachWebError::Golgi(err)
}
}
impl From<IoError> for PeachWebError {
fn from(err: IoError) -> PeachWebError {
PeachWebError::Io(err)
}
}
impl From<JsonError> for PeachWebError {
fn from(err: JsonError) -> PeachWebError {
PeachWebError::Json(err)
}
}
impl From<YamlError> for PeachWebError {
fn from(err: YamlError) -> PeachWebError {
PeachWebError::Yaml(err)
}
}
impl From<PeachError> for PeachWebError {
fn from(err: PeachError) -> PeachWebError {
PeachWebError::PeachLib {
@ -58,3 +85,9 @@ impl From<PeachError> for PeachWebError {
}
}
}
impl From<YamlError> for PeachWebError {
fn from(err: YamlError) -> PeachWebError {
PeachWebError::Yaml(err)
}
}

View File

@ -22,8 +22,6 @@
//! `Serialize`. The fields of the context object are available in the context
//! of the template to be rendered.
#![feature(proc_macro_hygiene, decl_macro)]
mod context;
pub mod error;
mod router;

View File

@ -1,13 +1,16 @@
use rocket::{catchers, fs::FileServer, routes, Build, Rocket};
use rocket_dyn_templates::Template;
use crate::routes::{
authentication::*,
catchers::*,
index::*,
scuttlebutt::*,
settings::{admin::*, dns::*, menu::*, network::*, scuttlebutt::*, theme::*},
status::{device::*, network::*, scuttlebutt::*},
use crate::{
routes::{
authentication::*,
catchers::*,
index::*,
scuttlebutt::*,
settings::{admin::*, dns::*, menu::*, network::*, scuttlebutt::*, theme::*},
status::{device::*, network::*, scuttlebutt::*},
},
utils,
};
/// Create a Rocket instance and mount PeachPub routes, fileserver and
@ -15,6 +18,10 @@ use crate::routes::{
/// settings and status routes related to networking and the device (memory,
/// hard disk, CPU etc.).
pub fn mount_peachpub_routes(rocket: Rocket<Build>) -> Rocket<Build> {
// set the `.ssb-go` path in order to mount the blob fileserver
let ssb_path = utils::get_go_ssb_path().expect("define ssb-go dir path");
let blobstore = format!("{}/blobs/sha256", ssb_path);
rocket
.mount(
"/",
@ -62,12 +69,29 @@ pub fn mount_peachpub_routes(rocket: Rocket<Build>) -> Rocket<Build> {
.mount(
"/scuttlebutt",
routes![
peers, friends, follows, followers, blocks, profile, private, follow, unfollow,
block, publish,
invites,
create_invite,
peers,
search,
search_post,
friends,
follows,
blocks,
profile,
update_profile,
update_profile_post,
private,
private_post,
follow,
unfollow,
block,
unblock,
publish,
],
)
.mount("/status", routes![scuttlebutt_status])
.mount("/", FileServer::from("static"))
.mount("/blob", FileServer::from(blobstore).rank(-1))
.register("/", catchers![not_found, internal_error, forbidden])
.attach(Template::fairing())
}

File diff suppressed because it is too large Load Diff

View File

@ -1,40 +1,37 @@
use peach_lib::sbot::{SbotConfig, SbotStatus};
use rocket::{get, State};
use rocket_dyn_templates::{tera::Context, Template};
use rocket_dyn_templates::Template;
use crate::routes::authentication::Authenticated;
use crate::utils;
use crate::RocketConfig;
use crate::{context::scuttlebutt::StatusContext, RocketConfig};
// HELPERS AND ROUTES FOR /status/scuttlebutt
#[get("/scuttlebutt")]
pub fn scuttlebutt_status(_auth: Authenticated, config: &State<RocketConfig>) -> Template {
// retrieve current ui theme
let theme = utils::get_theme();
pub async fn scuttlebutt_status(_auth: Authenticated, config: &State<RocketConfig>) -> Template {
let context = StatusContext::build().await;
// retrieve go-sbot systemd process status
let sbot_status = SbotStatus::read().ok();
// retrieve go-sbot configuration parameters
let sbot_config = SbotConfig::read().ok();
let mut context = Context::new();
context.insert("theme", &theme);
context.insert("sbot_status", &sbot_status);
context.insert("sbot_config", &sbot_config);
context.insert("flash_name", &None::<()>);
context.insert("flash_msg", &None::<()>);
context.insert("title", &Some("Scuttlebutt Status"));
// define back arrow url based on mode
if config.standalone_mode {
let back = if config.standalone_mode {
// return to home page
context.insert("back", &Some("/"));
Some("/".to_string())
} else {
// return to status menu
context.insert("back", &Some("/status"));
}
Some("/status".to_string())
};
Template::render("status/scuttlebutt", &context.into_json())
match context {
Ok(mut context) => {
// define back arrow url based on mode
context.back = back;
Template::render("status/scuttlebutt", &context)
}
Err(_) => {
let mut context = StatusContext::default();
// define back arrow url based on mode
context.back = back;
Template::render("status/scuttlebutt", &context)
}
}
}

View File

@ -1,11 +1,88 @@
pub mod monitor;
use log::info;
use rocket::response::{Redirect, Responder};
use rocket::serde::Serialize;
use rocket_dyn_templates::Template;
use std::io::prelude::*;
use std::{fs, fs::File, path::Path};
use crate::THEME;
use dirs;
use golgi::blobs;
use log::info;
use peach_lib::sbot::SbotConfig;
use rocket::{
fs::TempFile,
response::{Redirect, Responder},
serde::Serialize,
};
use rocket_dyn_templates::Template;
use temporary::Directory;
use crate::{error::PeachWebError, THEME};
// 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_else(|| 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(file: &mut TempFile<'_>) -> Result<String, PeachWebError> {
// 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?;
// 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)
}
// THEME FUNCTIONS

View File

@ -277,6 +277,18 @@ body {
padding-bottom: 1rem;
}
.capsule-profile {
margin-left: 1rem;
margin-right: 1rem;
}
@media only screen and (min-width: 600px) {
.capsule-profile {
margin-left: 0;
margin-right: 0;
}
}
@media only screen and (min-width: 600px) {
.capsule-container {
margin-left: 0;
@ -728,6 +740,7 @@ form {
height: 7rem;
overflow: auto;
resize: vertical;
width: 100%;
}
.alert-input {
@ -753,7 +766,7 @@ form {
font-family: var(--sans-serif);
font-size: var(--font-size-7);
display: block;
/* margin-bottom: 2px; */
margin-bottom: 2px;
}
.label-medium {

0
peach-web/static/icons/pencil.svg Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 957 B

After

Width:  |  Height:  |  Size: 957 B

0
peach-web/static/icons/user.svg Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -0,0 +1,20 @@
{%- extends "nav" -%}
{%- block card %}
<!-- SCUTTLEBUTT INVITE FORM -->
<div class="card center">
<form id="invites" class="center" action="/scuttlebutt/invites" method="post">
<div class="center" style="width: 80%;">
<label for="inviteUses" class="label-small font-gray" title="Number of times the invite code can be reused">USES</label>
<input type="number" id="inviteUses" name="uses" min="1" max="150" size="3" value="1">
{% if invite_code %}
<p class="card-text" style="margin-top: 1rem; user-select: all;" title="Invite code">{{ invite_code }}</p>
{% endif %}
</div>
<!-- BUTTONS -->
<input id="createInvite" class="button button-primary center" style="margin-top: 1rem;" type="submit" title="Create a new invite code" value="Create">
<a id="cancel" class="button button-secondary center" href="/scuttlebutt/peers" title="Cancel">Cancel</a>
</form>
<!-- FLASH MESSAGE -->
{% include "snippets/flash_message" %}
</div>
{%- endblock card -%}

View File

@ -1,10 +0,0 @@
{%- extends "nav" -%}
{%- block card %}
<!-- SCUTTLEBUTT MESSAGES -->
<div class="card center">
<div class="capsule capsule-container border-ssb">
<!-- FLASH MESSAGE -->
{% include "snippets/flash_message" %}
</div>
</div>
{%- endblock card -%}

View File

@ -5,10 +5,11 @@
<div class="card-container">
<!-- BUTTONS -->
<div id="buttons">
<a id="friends" class="button button-primary center" href="/scuttlebutt/friends" title="List Friends">Friends</a>
<a id="follows" class="button button-primary center" href="/scuttlebutt/follows" title="List Follows">Follows</a>
<a id="followers" class="button button-primary center" href="/scuttlebutt/followers" title="List Followers">Followers</a>
<a id="blocks" class="button button-primary center" href="/scuttlebutt/blocks" title="List Blocks">Blocks</a>
<a id="search" class="button button-primary center" href="/scuttlebutt/search" title="Search for a peer">Search</a>
<a id="friends" class="button button-primary center" href="/scuttlebutt/friends" title="List friends">Friends</a>
<a id="follows" class="button button-primary center" href="/scuttlebutt/follows" title="List follows">Follows</a>
<a id="blocks" class="button button-primary center" href="/scuttlebutt/blocks" title="List blocks">Blocks</a>
<a id="invites" class="button button-primary center" href="/scuttlebutt/invites" title="Create invites">Invites</a>
</div>
</div>
</div>

View File

@ -1,16 +1,24 @@
{%- extends "nav" -%}
{%- block card %}
<div class="card center">
{%- if peers %}
<ul class="list">
<!-- for peer in peers -->
{%- for peer in peers %}
<li>
<a class="list-item link light-bg" href="/scuttlebutt/profile?public_key=pub_key">
<img id="peerImage" class="icon list-icon" src="{ image_path }" alt="{ peer_name }'s profile image">
<p id="peerName" class="list-text">{ name }</p>
<label class="label-small label-ellipsis list-label font-gray" for="peerName" title="{ peer_name }'s Public Key">{ public_key }</label>
<a class="list-item link light-bg" href="/scuttlebutt/profile?public_key={{ peer['id'] }}">
{%- if peer['blob_path'] and peer['blob_exists'] == "true" %}
<img id="peerImage" class="icon list-icon" src="/blob/{{ peer['blob_path'] }}" alt="{{ peer['name'] }}'s profile image">
{%- else %}
<img id="peerImage" class="icon list-icon" src="/icons/user.svg" alt="Placeholder profile image">
{%- endif %}
<p id="peerName" class="font-normal list-text">{{ peer['name'] }}</p>
<label class="label-small label-ellipsis list-label font-gray" for="peerName" title="{{ peer['name'] }}'s Public Key">{{ peer['id'] }}</label>
</a>
</li>
<!-- end for loop -->
{%- endfor %}
</ul>
{%- else %}
<p>No follows found</p>
{%- endif %}
</div>
{%- endblock card -%}

View File

@ -0,0 +1,23 @@
{%- extends "nav" -%}
{%- block card %}
<!-- SCUTTLEBUTT PRIVATE MESSAGE FORM -->
<div class="card center">
{# only render the private message elements if the sbot is active #}
{%- if sbot_status and sbot_status.state == "active" %}
<form id="sbotConfig" class="center" action="/scuttlebutt/private" method="post">
<div class="center" style="display: flex; flex-direction: column; margin-bottom: 1rem;" title="Public key (ID) of the peer being written to">
<label for="publicKey" class="label-small font-gray">PUBLIC KEY</label>
<input type="text" id="publicKey" name="recipient" placeholder="@xYz...=.ed25519" {% if recipient_id %}value="{{ recipient_id }}"{% else %}autofocus{% endif %}>
</div>
<!-- input for message contents -->
<textarea id="privatePost" class="center input message-input" name="text" title="Compose a private message" placeholder="Write a private message..."{% if recipient_id %} autofocus{% endif %}></textarea>
<!-- hidden input field to pass the public key of the local peer -->
<input type="hidden" id="localId" name="id" value="{{ id }}">
<!-- BUTTONS -->
<input id="publish" class="button button-primary center" type="submit" style="margin-top: 1rem;" title="Publish private message to peer" value="Publish">
</form>
{%- endif %}
<!-- FLASH MESSAGE -->
{% include "snippets/flash_message" %}
</div>
{%- endblock card -%}

View File

@ -2,31 +2,71 @@
{%- block card %}
<!-- USER PROFILE -->
<div class="card center">
{# only render the profile info elements if the sbot is active #}
{%- if sbot_status and sbot_status.state == "active" %}
<!-- PROFILE INFO BOX -->
<div class="capsule capsule-profile" title="Scuttlebutt account profile information">
<div class="capsule capsule-profile border-ssb" title="Scuttlebutt account profile information">
{% if is_local_profile %}
<!-- edit profile button -->
<img id="editProfile" class="icon-small icon-active nav-icon-right" src="/icons/pencil.svg" alt="Profile picture">
<a class="nav-icon-right" href="/scuttlebutt/profile/update" title="Edit your profile">
<img id="editProfile" class="icon-small icon-active" src="/icons/pencil.svg" alt="Edit">
</a>
{% endif %}
<!-- PROFILE BIO -->
<!-- profile picture -->
<img id="profilePicture" class="icon-large" src="{ image_path }" alt="Profile picture">
{# only try to render profile pic if we have the blob #}
{%- if blob_path and blob_exists == true %}
<img id="profilePicture" class="icon-large" src="/blob/{{ blob_path }}" title="Profile picture" alt="Profile picture">
{% else %}
{# render a placeholder profile picture (icon) #}
<img id="profilePicture" class="icon" src="/icons/user.svg" title="Profile picture" alt="Profile picture">
{% endif %}
<!-- name, public key & description -->
<p id="profileName" class="card-text" title="Name">{ name }</p>
<label class="label-small label-ellipsis font-gray" style="user-select: all;" for="profileName" title="Public Key">{ public_key }</label>
<p id="profileDescription" style="margin-top: 1rem" class="card-text" title="Description">{ description }</p>
<p id="profileName" class="card-text" title="Name">{{ name }}</p>
<label class="label-small label-ellipsis font-gray" style="user-select: all;" for="profileName" title="Public Key">{{ id }}</label>
<p id="profileDescription" style="margin-top: 1rem" class="card-text" title="Description">{{ description }}</p>
</div>
{% if is_local_profile %}
<!-- PUBLIC POST FORM -->
<form id="postForm" class="center" action="/scuttlebutt/post" method="post">
<form id="postForm" class="center" action="/scuttlebutt/publish" method="post">
<!-- input for message contents -->
<textarea id="publicPost" class="center input message-input" title="Compose Public Post"></textarea>
<textarea id="publicPost" class="center input message-input" name="text" title="Compose Public Post" placeholder="Write a public post..."></textarea>
<input id="publishPost" class="button button-primary center" title="Publish" type="submit" value="Publish">
</form>
{% else %}
<!-- BUTTONS -->
<!-- TODO: each of these buttons needs to be a form with a public key -->
<div id="buttons" style="margin-top: 2rem;">
<a id="followPeer" class="button button-primary center" href="/scuttlebutt/follow" title="Follow Peer">Follow</a>
<a id="blockPeer" class="button button-warning center" href="/scuttlebutt/block" title="Block Peer">Block</a>
<a id="privateMessage" class="button button-primary center" href="/scuttlebutt/private_message" title="Private Message">Private Message</a>
{% if following == false %}
<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 %}
<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 %}
<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 %}
<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 %}
<a id="privateMessage" class="button button-primary center" href="/scuttlebutt/private?public_key={{ id }}" title="Private Message">Send Private Message</a>
</div>
{%- endif %}
{%- endif %}
<!-- FLASH MESSAGE -->
{% include "snippets/flash_message" %}
</div>

View File

@ -0,0 +1,16 @@
{%- extends "nav" -%}
{%- block card %}
<!-- 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...=.ed25519" autofocus>
</div>
<!-- BUTTONS -->
<input id="search" class="button button-primary center" type="submit" title="Search for peer" value="Search">
</form>
<!-- FLASH MESSAGE -->
{% include "snippets/flash_message" %}
</div>
{%- endblock card -%}

View File

@ -0,0 +1,27 @@
{%- extends "nav" -%}
{%- block card %}
{# ASSIGN VARIABLES #}
{# ---------------- #}
<!-- SSB PROFILE UPDATE FORM -->
<div class="card 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</label>
<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</label>
<textarea id="description" class="message-input" style="margin-bottom: 1rem;" name="new_description" placeholder="Write a description for your profile...">{{ description }}</textarea>
<label for="image" class="label-small font-gray">IMAGE</label>
<input type="file" id="fileInput" name="image">
</div>
<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</a>
</div>
</form>
<!-- FLASH MESSAGE -->
{% include "snippets/flash_message" %}
</div>
{%- endblock card -%}

View File

@ -5,6 +5,9 @@
{%- elif flash_msg and flash_name == "info" %}
<!-- display info message -->
<div class="capsule center-text flash-message font-normal border-info">{{ flash_msg }}.</div>
{%- elif flash_msg and flash_name == "warning" %}
<!-- display warning message -->
<div class="capsule center-text flash-message font-normal border-warning">{{ flash_msg }}.</div>
{%- elif flash_msg and flash_name == "error" %}
<!-- display error message -->
<div class="capsule center-text flash-message font-normal border-danger">{{ flash_msg }}.</div>

View File

@ -3,12 +3,12 @@
{# ASSIGN VARIABLES #}
{# ---------------- #}
{%- if sbot_status.memory -%}
{% set mem = sbot_status.memory / 1024 / 1024 | round -%}
{% set mem = sbot_status.memory / 1024 / 1024 | round | int -%}
{%- else -%}
{% set mem = "X" -%}
{%- endif -%}
{%- if sbot_status.blobstore -%}
{% set blobs = sbot_status.blobstore / 1024 / 1024 | round -%}
{% set blobs = sbot_status.blobstore / 1024 / 1024 | round | int -%}
{%- else -%}
{% set blobs = "X" -%}
{%- endif -%}
@ -52,33 +52,13 @@
</div>
<hr style="color: var(--light-gray);">
<div id="middleSection" style="margin-top: 1rem;">
<div id="ssbSocialCounts" class="center" style="display: flex; justify-content: space-between; width: 90%;">
<div style="display: flex; align-items: last baseline;">
<label class="card-text" style="margin-right: 2px;">21</label>
<label class="label-small font-gray">Friends</label>
</div>
<div style="display: flex; align-items: last baseline;">
<label class="card-text" style="margin-right: 2px;">5</label>
<label class="label-small font-gray">Follows</label>
</div>
<div style="display: flex; align-items: last baseline;">
<label class="card-text" style="margin-right: 2px;">38</label>
<label class="label-small font-gray">Followers</label>
</div>
<div style="display: flex; align-items: last baseline;">
<label class="card-text" style="margin-right: 2px;">2</label>
<label class="label-small font-gray">Blocks</label>
<div id="sbotInfo" class="center" style="display: flex; justify-content: space-between; width: 90%;">
<div class="center" style="display: flex; align-items: last baseline;">
<label class="card-text" style="margin-right: 5px;">{{ latest_seq }}</label>
<label class="label-small font-gray">MESSAGES IN LOCAL DATABASE</label>
</div>
</div>
</div>
<!--
not implemented yet:
<p class="card-text">Latest sequence number: </p>
<p class="card-text">Network key: </p>
<p>Blobstore size: </p>
<p>Last time you visited this page, latest sequence was x ... now it's y</p>
<p>Number of follows / followers / friends / blocks</p>
-->
<hr style="color: var(--light-gray);">
<!-- THREE-ACROSS STACK -->
<div class="three-grid card-container" style="margin-top: 1rem;">