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, ) -> Result { // 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, pub flash_name: Option, pub flash_msg: Option, pub title: Option, pub theme: Option, pub sbot_config: Option, pub sbot_status: Option, // latest sequence number for the local log pub latest_seq: Option, } 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 { 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 = 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, pub flash_name: Option, pub flash_msg: Option, pub title: Option, pub theme: Option, pub sbot_config: Option, pub sbot_status: Option, pub peers: Option>>, } 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 { 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, pub flash_name: Option, pub flash_msg: Option, pub title: Option, pub theme: Option, pub sbot_config: Option, pub sbot_status: Option, pub peers: Option>>, } 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 { 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, pub flash_name: Option, pub flash_msg: Option, pub title: Option, pub theme: Option, pub sbot_config: Option, pub sbot_status: Option, pub peers: Option>>, } 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 { 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, pub flash_name: Option, pub flash_msg: Option, pub title: Option, pub theme: Option, pub sbot_config: Option, pub sbot_status: Option, // 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, pub name: Option, pub description: Option, pub image: Option, // the path to the blob defined in the `image` field (aka the profile picture) pub blob_path: Option, // 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, pub blocking: Option, } 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) -> Result { 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, pub flash_name: Option, pub flash_msg: Option, pub title: Option, pub theme: Option, pub sbot_config: Option, pub sbot_status: Option, // local peer id (whoami) pub id: Option, // id of the peer being messaged pub recipient_id: Option, } 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) -> Result { 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) } }