Merge pull request 'Integrate golgi into peach_web' (#81) from golgi_integration into main
Reviewed-on: #81
This commit is contained in:
644
Cargo.lock
generated
644
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -35,7 +35,11 @@ travis-ci = { repository = "peachcloud/peach-web", branch = "master" }
|
|||||||
maintenance = { status = "actively-developed" }
|
maintenance = { status = "actively-developed" }
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
base64 = "0.13.0"
|
||||||
|
dirs = "4.0.0"
|
||||||
env_logger = "0.8"
|
env_logger = "0.8"
|
||||||
|
#golgi = "0.1.0"
|
||||||
|
golgi = { path = "../../../playground/rust/golgi" }
|
||||||
lazy_static = "1.4.0"
|
lazy_static = "1.4.0"
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
nest = "1.0.0"
|
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"] }
|
rocket = { version = "0.5.0-rc.1", features = ["json", "secrets"] }
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
|
temporary = "0.6.4"
|
||||||
tera = { version = "1.12.1", features = ["builtins"] }
|
tera = { version = "1.12.1", features = ["builtins"] }
|
||||||
xdg = "2.2.0"
|
xdg = "2.2.0"
|
||||||
|
|
||||||
|
@ -1,2 +1,3 @@
|
|||||||
pub mod dns;
|
pub mod dns;
|
||||||
pub mod network;
|
pub mod network;
|
||||||
|
pub mod scuttlebutt;
|
||||||
|
588
peach-web/src/context/scuttlebutt.rs
Normal file
588
peach-web/src/context/scuttlebutt.rs
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
@ -1,5 +1,8 @@
|
|||||||
//! Custom error type representing all possible error variants for peach-web.
|
//! 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::error::PeachError;
|
||||||
use peach_lib::{serde_json, serde_yaml};
|
use peach_lib::{serde_json, serde_yaml};
|
||||||
use serde_json::error::Error as JsonError;
|
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.
|
/// Custom error type encapsulating all possible errors for the web application.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum PeachWebError {
|
pub enum PeachWebError {
|
||||||
Json(JsonError),
|
|
||||||
Yaml(YamlError),
|
|
||||||
FailedToRegisterDynDomain(String),
|
FailedToRegisterDynDomain(String),
|
||||||
|
Golgi(GolgiError),
|
||||||
|
HomeDir,
|
||||||
|
Io(IoError),
|
||||||
|
Json(JsonError),
|
||||||
|
OsString,
|
||||||
PeachLib { source: PeachError, msg: String },
|
PeachLib { source: PeachError, msg: String },
|
||||||
|
Yaml(YamlError),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::error::Error for PeachWebError {
|
impl std::error::Error for PeachWebError {
|
||||||
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
|
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
|
||||||
match *self {
|
match *self {
|
||||||
PeachWebError::Json(ref source) => Some(source),
|
|
||||||
PeachWebError::Yaml(ref source) => Some(source),
|
|
||||||
PeachWebError::FailedToRegisterDynDomain(_) => None,
|
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::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 {
|
impl std::fmt::Display for PeachWebError {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||||
match *self {
|
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) => {
|
PeachWebError::FailedToRegisterDynDomain(ref msg) => {
|
||||||
write!(f, "DYN DNS error: {}", 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::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 {
|
impl From<JsonError> for PeachWebError {
|
||||||
fn from(err: JsonError) -> PeachWebError {
|
fn from(err: JsonError) -> PeachWebError {
|
||||||
PeachWebError::Json(err)
|
PeachWebError::Json(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<YamlError> for PeachWebError {
|
|
||||||
fn from(err: YamlError) -> PeachWebError {
|
|
||||||
PeachWebError::Yaml(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<PeachError> for PeachWebError {
|
impl From<PeachError> for PeachWebError {
|
||||||
fn from(err: PeachError) -> PeachWebError {
|
fn from(err: PeachError) -> PeachWebError {
|
||||||
PeachWebError::PeachLib {
|
PeachWebError::PeachLib {
|
||||||
@ -58,3 +85,9 @@ impl From<PeachError> for PeachWebError {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<YamlError> for PeachWebError {
|
||||||
|
fn from(err: YamlError) -> PeachWebError {
|
||||||
|
PeachWebError::Yaml(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -22,8 +22,6 @@
|
|||||||
//! `Serialize`. The fields of the context object are available in the context
|
//! `Serialize`. The fields of the context object are available in the context
|
||||||
//! of the template to be rendered.
|
//! of the template to be rendered.
|
||||||
|
|
||||||
#![feature(proc_macro_hygiene, decl_macro)]
|
|
||||||
|
|
||||||
mod context;
|
mod context;
|
||||||
pub mod error;
|
pub mod error;
|
||||||
mod router;
|
mod router;
|
||||||
|
@ -1,13 +1,16 @@
|
|||||||
use rocket::{catchers, fs::FileServer, routes, Build, Rocket};
|
use rocket::{catchers, fs::FileServer, routes, Build, Rocket};
|
||||||
use rocket_dyn_templates::Template;
|
use rocket_dyn_templates::Template;
|
||||||
|
|
||||||
use crate::routes::{
|
use crate::{
|
||||||
authentication::*,
|
routes::{
|
||||||
catchers::*,
|
authentication::*,
|
||||||
index::*,
|
catchers::*,
|
||||||
scuttlebutt::*,
|
index::*,
|
||||||
settings::{admin::*, dns::*, menu::*, network::*, scuttlebutt::*, theme::*},
|
scuttlebutt::*,
|
||||||
status::{device::*, network::*, scuttlebutt::*},
|
settings::{admin::*, dns::*, menu::*, network::*, scuttlebutt::*, theme::*},
|
||||||
|
status::{device::*, network::*, scuttlebutt::*},
|
||||||
|
},
|
||||||
|
utils,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Create a Rocket instance and mount PeachPub routes, fileserver and
|
/// 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,
|
/// settings and status routes related to networking and the device (memory,
|
||||||
/// hard disk, CPU etc.).
|
/// hard disk, CPU etc.).
|
||||||
pub fn mount_peachpub_routes(rocket: Rocket<Build>) -> Rocket<Build> {
|
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
|
rocket
|
||||||
.mount(
|
.mount(
|
||||||
"/",
|
"/",
|
||||||
@ -62,12 +69,29 @@ pub fn mount_peachpub_routes(rocket: Rocket<Build>) -> Rocket<Build> {
|
|||||||
.mount(
|
.mount(
|
||||||
"/scuttlebutt",
|
"/scuttlebutt",
|
||||||
routes![
|
routes![
|
||||||
peers, friends, follows, followers, blocks, profile, private, follow, unfollow,
|
invites,
|
||||||
block, publish,
|
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("/status", routes![scuttlebutt_status])
|
||||||
.mount("/", FileServer::from("static"))
|
.mount("/", FileServer::from("static"))
|
||||||
|
.mount("/blob", FileServer::from(blobstore).rank(-1))
|
||||||
.register("/", catchers![not_found, internal_error, forbidden])
|
.register("/", catchers![not_found, internal_error, forbidden])
|
||||||
.attach(Template::fairing())
|
.attach(Template::fairing())
|
||||||
}
|
}
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -1,40 +1,37 @@
|
|||||||
use peach_lib::sbot::{SbotConfig, SbotStatus};
|
|
||||||
use rocket::{get, State};
|
use rocket::{get, State};
|
||||||
use rocket_dyn_templates::{tera::Context, Template};
|
use rocket_dyn_templates::Template;
|
||||||
|
|
||||||
use crate::routes::authentication::Authenticated;
|
use crate::routes::authentication::Authenticated;
|
||||||
use crate::utils;
|
use crate::{context::scuttlebutt::StatusContext, RocketConfig};
|
||||||
use crate::RocketConfig;
|
|
||||||
|
|
||||||
// HELPERS AND ROUTES FOR /status/scuttlebutt
|
// HELPERS AND ROUTES FOR /status/scuttlebutt
|
||||||
|
|
||||||
#[get("/scuttlebutt")]
|
#[get("/scuttlebutt")]
|
||||||
pub fn scuttlebutt_status(_auth: Authenticated, config: &State<RocketConfig>) -> Template {
|
pub async fn scuttlebutt_status(_auth: Authenticated, config: &State<RocketConfig>) -> Template {
|
||||||
// retrieve current ui theme
|
let context = StatusContext::build().await;
|
||||||
let theme = utils::get_theme();
|
|
||||||
|
|
||||||
// retrieve go-sbot systemd process status
|
let back = if config.standalone_mode {
|
||||||
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 {
|
|
||||||
// return to home page
|
// return to home page
|
||||||
context.insert("back", &Some("/"));
|
Some("/".to_string())
|
||||||
} else {
|
} else {
|
||||||
// return to status menu
|
// 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,11 +1,88 @@
|
|||||||
pub mod monitor;
|
pub mod monitor;
|
||||||
|
|
||||||
use log::info;
|
use std::io::prelude::*;
|
||||||
use rocket::response::{Redirect, Responder};
|
use std::{fs, fs::File, path::Path};
|
||||||
use rocket::serde::Serialize;
|
|
||||||
use rocket_dyn_templates::Template;
|
|
||||||
|
|
||||||
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
|
// THEME FUNCTIONS
|
||||||
|
|
||||||
|
@ -277,6 +277,18 @@ body {
|
|||||||
padding-bottom: 1rem;
|
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) {
|
@media only screen and (min-width: 600px) {
|
||||||
.capsule-container {
|
.capsule-container {
|
||||||
margin-left: 0;
|
margin-left: 0;
|
||||||
@ -728,6 +740,7 @@ form {
|
|||||||
height: 7rem;
|
height: 7rem;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
resize: vertical;
|
resize: vertical;
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.alert-input {
|
.alert-input {
|
||||||
@ -753,7 +766,7 @@ form {
|
|||||||
font-family: var(--sans-serif);
|
font-family: var(--sans-serif);
|
||||||
font-size: var(--font-size-7);
|
font-size: var(--font-size-7);
|
||||||
display: block;
|
display: block;
|
||||||
/* margin-bottom: 2px; */
|
margin-bottom: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.label-medium {
|
.label-medium {
|
||||||
|
0
peach-web/static/icons/pencil.svg
Executable file → Normal file
0
peach-web/static/icons/pencil.svg
Executable file → Normal file
Before Width: | Height: | Size: 957 B After Width: | Height: | Size: 957 B |
0
peach-web/static/icons/user.svg
Executable file → Normal file
0
peach-web/static/icons/user.svg
Executable file → Normal file
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 1.0 KiB |
20
peach-web/templates/scuttlebutt/invites.html.tera
Normal file
20
peach-web/templates/scuttlebutt/invites.html.tera
Normal 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 -%}
|
@ -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 -%}
|
|
@ -5,10 +5,11 @@
|
|||||||
<div class="card-container">
|
<div class="card-container">
|
||||||
<!-- BUTTONS -->
|
<!-- BUTTONS -->
|
||||||
<div id="buttons">
|
<div id="buttons">
|
||||||
<a id="friends" class="button button-primary center" href="/scuttlebutt/friends" title="List Friends">Friends</a>
|
<a id="search" class="button button-primary center" href="/scuttlebutt/search" title="Search for a peer">Search</a>
|
||||||
<a id="follows" class="button button-primary center" href="/scuttlebutt/follows" title="List Follows">Follows</a>
|
<a id="friends" class="button button-primary center" href="/scuttlebutt/friends" title="List friends">Friends</a>
|
||||||
<a id="followers" class="button button-primary center" href="/scuttlebutt/followers" title="List Followers">Followers</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="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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,16 +1,24 @@
|
|||||||
{%- extends "nav" -%}
|
{%- extends "nav" -%}
|
||||||
{%- block card %}
|
{%- block card %}
|
||||||
<div class="card center">
|
<div class="card center">
|
||||||
|
{%- if peers %}
|
||||||
<ul class="list">
|
<ul class="list">
|
||||||
<!-- for peer in peers -->
|
{%- for peer in peers %}
|
||||||
<li>
|
<li>
|
||||||
<a class="list-item link light-bg" href="/scuttlebutt/profile?public_key=pub_key">
|
<a class="list-item link light-bg" href="/scuttlebutt/profile?public_key={{ peer['id'] }}">
|
||||||
<img id="peerImage" class="icon list-icon" src="{ image_path }" alt="{ peer_name }'s profile image">
|
{%- if peer['blob_path'] and peer['blob_exists'] == "true" %}
|
||||||
<p id="peerName" class="list-text">{ name }</p>
|
<img id="peerImage" class="icon list-icon" src="/blob/{{ peer['blob_path'] }}" alt="{{ peer['name'] }}'s profile image">
|
||||||
<label class="label-small label-ellipsis list-label font-gray" for="peerName" title="{ peer_name }'s Public Key">{ public_key }</label>
|
{%- 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>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<!-- end for loop -->
|
{%- endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
|
{%- else %}
|
||||||
|
<p>No follows found</p>
|
||||||
|
{%- endif %}
|
||||||
</div>
|
</div>
|
||||||
{%- endblock card -%}
|
{%- endblock card -%}
|
||||||
|
23
peach-web/templates/scuttlebutt/private.html.tera
Normal file
23
peach-web/templates/scuttlebutt/private.html.tera
Normal 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 -%}
|
@ -2,31 +2,71 @@
|
|||||||
{%- block card %}
|
{%- block card %}
|
||||||
<!-- USER PROFILE -->
|
<!-- USER PROFILE -->
|
||||||
<div class="card center">
|
<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 -->
|
<!-- 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 -->
|
<!-- 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 BIO -->
|
||||||
<!-- profile picture -->
|
<!-- 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 -->
|
<!-- name, public key & description -->
|
||||||
<p id="profileName" class="card-text" title="Name">{ name }</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">{ public_key }</label>
|
<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>
|
<p id="profileDescription" style="margin-top: 1rem" class="card-text" title="Description">{{ description }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
{% if is_local_profile %}
|
||||||
<!-- PUBLIC POST FORM -->
|
<!-- 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 -->
|
<!-- 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">
|
<input id="publishPost" class="button button-primary center" title="Publish" type="submit" value="Publish">
|
||||||
</form>
|
</form>
|
||||||
|
{% else %}
|
||||||
<!-- BUTTONS -->
|
<!-- BUTTONS -->
|
||||||
<!-- TODO: each of these buttons needs to be a form with a public key -->
|
<!-- TODO: each of these buttons needs to be a form with a public key -->
|
||||||
<div id="buttons" style="margin-top: 2rem;">
|
<div id="buttons" style="margin-top: 2rem;">
|
||||||
<a id="followPeer" class="button button-primary center" href="/scuttlebutt/follow" title="Follow Peer">Follow</a>
|
{% if following == false %}
|
||||||
<a id="blockPeer" class="button button-warning center" href="/scuttlebutt/block" title="Block Peer">Block</a>
|
<form id="followForm" action="/scuttlebutt/follow" method="post">
|
||||||
<a id="privateMessage" class="button button-primary center" href="/scuttlebutt/private_message" title="Private Message">Private Message</a>
|
<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>
|
</div>
|
||||||
|
{%- endif %}
|
||||||
|
{%- endif %}
|
||||||
<!-- FLASH MESSAGE -->
|
<!-- FLASH MESSAGE -->
|
||||||
{% include "snippets/flash_message" %}
|
{% include "snippets/flash_message" %}
|
||||||
</div>
|
</div>
|
||||||
|
16
peach-web/templates/scuttlebutt/search.html.tera
Normal file
16
peach-web/templates/scuttlebutt/search.html.tera
Normal 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 -%}
|
27
peach-web/templates/scuttlebutt/update_profile.html.tera
Normal file
27
peach-web/templates/scuttlebutt/update_profile.html.tera
Normal 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 -%}
|
@ -5,6 +5,9 @@
|
|||||||
{%- elif flash_msg and flash_name == "info" %}
|
{%- elif flash_msg and flash_name == "info" %}
|
||||||
<!-- display info message -->
|
<!-- display info message -->
|
||||||
<div class="capsule center-text flash-message font-normal border-info">{{ flash_msg }}.</div>
|
<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" %}
|
{%- elif flash_msg and flash_name == "error" %}
|
||||||
<!-- display error message -->
|
<!-- display error message -->
|
||||||
<div class="capsule center-text flash-message font-normal border-danger">{{ flash_msg }}.</div>
|
<div class="capsule center-text flash-message font-normal border-danger">{{ flash_msg }}.</div>
|
||||||
|
@ -3,12 +3,12 @@
|
|||||||
{# ASSIGN VARIABLES #}
|
{# ASSIGN VARIABLES #}
|
||||||
{# ---------------- #}
|
{# ---------------- #}
|
||||||
{%- if sbot_status.memory -%}
|
{%- if sbot_status.memory -%}
|
||||||
{% set mem = sbot_status.memory / 1024 / 1024 | round -%}
|
{% set mem = sbot_status.memory / 1024 / 1024 | round | int -%}
|
||||||
{%- else -%}
|
{%- else -%}
|
||||||
{% set mem = "X" -%}
|
{% set mem = "X" -%}
|
||||||
{%- endif -%}
|
{%- endif -%}
|
||||||
{%- if sbot_status.blobstore -%}
|
{%- if sbot_status.blobstore -%}
|
||||||
{% set blobs = sbot_status.blobstore / 1024 / 1024 | round -%}
|
{% set blobs = sbot_status.blobstore / 1024 / 1024 | round | int -%}
|
||||||
{%- else -%}
|
{%- else -%}
|
||||||
{% set blobs = "X" -%}
|
{% set blobs = "X" -%}
|
||||||
{%- endif -%}
|
{%- endif -%}
|
||||||
@ -52,33 +52,13 @@
|
|||||||
</div>
|
</div>
|
||||||
<hr style="color: var(--light-gray);">
|
<hr style="color: var(--light-gray);">
|
||||||
<div id="middleSection" style="margin-top: 1rem;">
|
<div id="middleSection" style="margin-top: 1rem;">
|
||||||
<div id="ssbSocialCounts" class="center" style="display: flex; justify-content: space-between; width: 90%;">
|
<div id="sbotInfo" class="center" style="display: flex; justify-content: space-between; width: 90%;">
|
||||||
<div style="display: flex; align-items: last baseline;">
|
<div class="center" style="display: flex; align-items: last baseline;">
|
||||||
<label class="card-text" style="margin-right: 2px;">21</label>
|
<label class="card-text" style="margin-right: 5px;">{{ latest_seq }}</label>
|
||||||
<label class="label-small font-gray">Friends</label>
|
<label class="label-small font-gray">MESSAGES IN LOCAL DATABASE</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>
|
</div>
|
||||||
</div>
|
</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);">
|
<hr style="color: var(--light-gray);">
|
||||||
<!-- THREE-ACROSS STACK -->
|
<!-- THREE-ACROSS STACK -->
|
||||||
<div class="three-grid card-container" style="margin-top: 1rem;">
|
<div class="three-grid card-container" style="margin-top: 1rem;">
|
||||||
|
Reference in New Issue
Block a user