add profile template and route handler
This commit is contained in:
parent
602c6a90f1
commit
85231a20c7
|
@ -1,3 +1,4 @@
|
||||||
|
use log::debug;
|
||||||
use rouille::{router, Request, Response};
|
use rouille::{router, Request, Response};
|
||||||
|
|
||||||
use crate::{routes, utils::flash::FlashResponse};
|
use crate::{routes, utils::flash::FlashResponse};
|
||||||
|
@ -119,6 +120,15 @@ pub fn mount_peachpub_routes(request: &Request) -> Response {
|
||||||
Response::html(routes::scuttlebutt::peers::build_template())
|
Response::html(routes::scuttlebutt::peers::build_template())
|
||||||
},
|
},
|
||||||
|
|
||||||
|
(GET) (/scuttlebutt/profile) => {
|
||||||
|
Response::html(routes::scuttlebutt::profile::build_template(request, None))
|
||||||
|
},
|
||||||
|
|
||||||
|
(GET) (/scuttlebutt/profile/{ssb_id: String}) => {
|
||||||
|
debug!("ssb_id: {}", ssb_id);
|
||||||
|
Response::html(routes::scuttlebutt::profile::build_template(request, Some(ssb_id)))
|
||||||
|
},
|
||||||
|
|
||||||
(GET) (/scuttlebutt/search) => {
|
(GET) (/scuttlebutt/search) => {
|
||||||
Response::html(routes::scuttlebutt::search::build_template(request))
|
Response::html(routes::scuttlebutt::search::build_template(request))
|
||||||
.reset_flash()
|
.reset_flash()
|
||||||
|
|
|
@ -0,0 +1,180 @@
|
||||||
|
use maud::{html, Markup, PreEscaped};
|
||||||
|
use peach_lib::sbot::SbotStatus;
|
||||||
|
use rouille::Request;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
templates,
|
||||||
|
utils::{flash::FlashRequest, sbot, sbot::Profile},
|
||||||
|
};
|
||||||
|
|
||||||
|
// ROUTE: /scuttlebutt/profile
|
||||||
|
|
||||||
|
fn public_post_form_template() -> Markup {
|
||||||
|
html! {
|
||||||
|
(PreEscaped("<!-- PUBLIC POST FORM -->"))
|
||||||
|
form id="postForm" class="center" action="/scuttlebutt/publish" method="post" {
|
||||||
|
(PreEscaped("<!-- input for message contents -->"))
|
||||||
|
textarea id="publicPost" class="center input message-input" name="text" title="Compose Public Post" placeholder="Write a public post..." { }
|
||||||
|
input id="publishPost" class="button button-primary center" title="Publish" type="submit" value="Publish";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn profile_info_box_template(profile: &Profile) -> Markup {
|
||||||
|
html! {
|
||||||
|
(PreEscaped("<!-- PROFILE INFO BOX -->"))
|
||||||
|
div class="capsule capsule-profile border-ssb" title="Scuttlebutt account profile information" {
|
||||||
|
@if profile.is_local_profile {
|
||||||
|
(PreEscaped("<!-- edit profile button -->"))
|
||||||
|
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";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// render the profile bio: picture, id, name, image & description
|
||||||
|
(profile_bio_template(&profile))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn profile_bio_template(profile: &Profile) -> Markup {
|
||||||
|
html! {
|
||||||
|
(PreEscaped("<!-- PROFILE BIO -->"))
|
||||||
|
(PreEscaped("<!-- profile picture -->"))
|
||||||
|
// only try to render profile pic if we have the blob
|
||||||
|
@match &profile.blob_path {
|
||||||
|
Some(blob_path) if profile.blob_exists => {
|
||||||
|
img id="profilePicture" class="icon-large" src={ "/blob/" (blob_path) } title="Profile picture" alt="Profile picture";
|
||||||
|
},
|
||||||
|
_ => {
|
||||||
|
// use a placeholder image if we don't have the blob
|
||||||
|
img id="peerImage" class="icon icon-active list-icon" src="/icons/user.svg" alt="Placeholder profile image";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(PreEscaped("<!-- name, public key & description -->"))
|
||||||
|
p id="profileName" class="card-text" title="Name" {
|
||||||
|
@if let Some(name) = &profile.name {
|
||||||
|
(name)
|
||||||
|
} @else {
|
||||||
|
"Name unavailable"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
label class="label-small label-ellipsis font-gray" style="user-select: all;" for="profileName" title="Public Key" {
|
||||||
|
@if let Some(id) = &profile.id {
|
||||||
|
(id)
|
||||||
|
} @else {
|
||||||
|
"Public key unavailable"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
p id="profileDescription" style="margin-top: 1rem" class="card-text" title="Description" {
|
||||||
|
@if let Some(description) = &profile.description {
|
||||||
|
(description)
|
||||||
|
} @else {
|
||||||
|
"Description unavailable"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn social_interaction_buttons_template(profile: &Profile) -> Markup {
|
||||||
|
html! {
|
||||||
|
(PreEscaped("<!-- BUTTONS -->"))
|
||||||
|
div id="buttons" style="margin-top: 2rem;" {
|
||||||
|
@match (profile.following, &profile.id) {
|
||||||
|
(Some(false), Some(ssb_id)) => {
|
||||||
|
form id="followForm" class="center" action="/scuttlebutt/follow" method="post" {
|
||||||
|
input type="hidden" id="publicKey" name="public_key" value=(ssb_id);
|
||||||
|
input id="followPeer" class="button button-primary center" type="submit" title="Follow Peer" value="Follow";
|
||||||
|
}
|
||||||
|
},
|
||||||
|
(Some(true), Some(ssb_id)) => {
|
||||||
|
form id="unfollowForm" class="center" action="/scuttlebutt/unfollow" method="post" {
|
||||||
|
input type="hidden" id="publicKey" name="public_key" value=(ssb_id);
|
||||||
|
input id="unfollowPeer" class="button button-primary center" type="submit" title="Unfollow Peer" value="Unfollow";
|
||||||
|
}
|
||||||
|
},
|
||||||
|
_ => p { "Unable to determine follow state" }
|
||||||
|
}
|
||||||
|
@match (profile.blocking, &profile.id) {
|
||||||
|
(Some(false), Some(ssb_id)) => {
|
||||||
|
form id="blockForm" class="center" action="/scuttlebutt/block" method="post" {
|
||||||
|
input type="hidden" id="publicKey" name="public_key" value=(ssb_id);
|
||||||
|
input id="blockPeer" class="button button-primary center" type="submit" title="Block Peer" value="Block";
|
||||||
|
}
|
||||||
|
},
|
||||||
|
(Some(true), Some(ssb_id)) => {
|
||||||
|
form id="unblockForm" class="center" action="/scuttlebutt/unblock" method="post" {
|
||||||
|
input type="hidden" id="publicKey" name="public_key" value=(ssb_id);
|
||||||
|
input id="unblockPeer" class="button button-primary center" type="submit" title="Unblock Peer" value="Unblock";
|
||||||
|
}
|
||||||
|
},
|
||||||
|
_ => p { "Unable to determine block state" }
|
||||||
|
}
|
||||||
|
@if let Some(ssb_id) = &profile.id {
|
||||||
|
form class="center" {
|
||||||
|
a id="privateMessage" class="button button-primary center" href={ "/scuttlebutt/private?public_key=" (ssb_id) } title="Private Message" {
|
||||||
|
"Send Private Message"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Scuttlebutt profile template builder.
|
||||||
|
///
|
||||||
|
/// Render a Scuttlebutt profile, either for the local profile or for a peer
|
||||||
|
/// specified by a public key. If the public key query parameter is not
|
||||||
|
/// provided, the local profile is displayed (ie. the profile of the public key
|
||||||
|
/// associated with the local PeachCloud device).
|
||||||
|
pub fn build_template(request: &Request, ssb_id: Option<String>) -> PreEscaped<String> {
|
||||||
|
// check for flash cookies; will be (None, None) if no flash cookies are found
|
||||||
|
let (flash_name, flash_msg) = request.retrieve_flash();
|
||||||
|
|
||||||
|
let profile_template = match SbotStatus::read() {
|
||||||
|
Ok(status) if status.state == Some("active".to_string()) => {
|
||||||
|
// TODO: validate ssb_id and return error template
|
||||||
|
|
||||||
|
// retrieve the profile info
|
||||||
|
match sbot::get_profile_info(ssb_id) {
|
||||||
|
Ok(profile) => {
|
||||||
|
// render the profile template
|
||||||
|
html! {
|
||||||
|
(PreEscaped("<!-- SSB PROFILE -->"))
|
||||||
|
div class="card card-wide center" {
|
||||||
|
// render profile info box
|
||||||
|
(profile_info_box_template(&profile))
|
||||||
|
@if profile.is_local_profile {
|
||||||
|
// render the public post form template
|
||||||
|
(public_post_form_template())
|
||||||
|
} @else {
|
||||||
|
// render follow / unfollow, block / unblock and
|
||||||
|
// private message buttons
|
||||||
|
(social_interaction_buttons_template(&profile))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// render flash message if cookies were found in the request
|
||||||
|
@if let (Some(name), Some(msg)) = (flash_name, flash_msg) {
|
||||||
|
(PreEscaped("<!-- FLASH MESSAGE -->"))
|
||||||
|
(templates::flash::build_template(name, msg))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
// render the sbot error template with the error message
|
||||||
|
let error_template = templates::error::build_template(e.to_string());
|
||||||
|
// wrap the nav bars around the error template content
|
||||||
|
let body = templates::nav::build_template(error_template, "Profile", Some("/"));
|
||||||
|
|
||||||
|
// render the base template with the provided body
|
||||||
|
templates::base::build_template(body)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => templates::inactive::build_template("Profile is unavailable."),
|
||||||
|
};
|
||||||
|
|
||||||
|
let body = templates::nav::build_template(profile_template, "Profile", Some("/"));
|
||||||
|
|
||||||
|
templates::base::build_template(body)
|
||||||
|
}
|
|
@ -120,6 +120,132 @@ pub fn create_invite(uses: u16) -> Result<String, Box<dyn Error>> {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Profile {
|
||||||
|
// is this the local profile or the profile of a peer?
|
||||||
|
pub is_local_profile: bool,
|
||||||
|
// an ssb_id which may or may not be the local public key
|
||||||
|
pub id: Option<String>,
|
||||||
|
pub name: Option<String>,
|
||||||
|
pub description: Option<String>,
|
||||||
|
pub image: Option<String>,
|
||||||
|
// the path to the blob defined in the `image` field (aka the profile picture)
|
||||||
|
pub blob_path: Option<String>,
|
||||||
|
// whether or not the blob exists in the blobstore (ie. is saved on disk)
|
||||||
|
pub blob_exists: bool,
|
||||||
|
// relationship state (if the profile being viewed is not for the local public key)
|
||||||
|
pub following: Option<bool>,
|
||||||
|
pub blocking: Option<bool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Profile {
|
||||||
|
pub fn default() -> Self {
|
||||||
|
Profile {
|
||||||
|
is_local_profile: true,
|
||||||
|
id: None,
|
||||||
|
name: None,
|
||||||
|
description: None,
|
||||||
|
image: None,
|
||||||
|
blob_path: None,
|
||||||
|
blob_exists: false,
|
||||||
|
following: None,
|
||||||
|
blocking: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Retrieve the profile info for the given public key.
|
||||||
|
pub fn get_profile_info(ssb_id: Option<String>) -> Result<Profile, Box<dyn Error>> {
|
||||||
|
// retrieve latest go-sbot configuration parameters
|
||||||
|
let sbot_config = SbotConfig::read().ok();
|
||||||
|
|
||||||
|
task::block_on(async {
|
||||||
|
let mut sbot_client = init_sbot_with_config(&sbot_config).await?;
|
||||||
|
|
||||||
|
let local_id = sbot_client.whoami().await?;
|
||||||
|
|
||||||
|
let mut profile = Profile::default();
|
||||||
|
|
||||||
|
// if an ssb_id has been provided, we assume that the profile info
|
||||||
|
// being retrieved is for a peer (ie. not for our local profile)
|
||||||
|
let id = if ssb_id.is_some() {
|
||||||
|
// we are not dealing with the local profile
|
||||||
|
profile.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
|
||||||
|
profile.following = match sbot_client.friends_is_following(follow_query).await {
|
||||||
|
Ok(following) if following == "true" => Some(true),
|
||||||
|
Ok(following) if following == "false" => Some(false),
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO: i don't like that we have to instantiate the same query object
|
||||||
|
// twice. see if we can streamline this in golgi
|
||||||
|
let block_query = RelationshipQuery {
|
||||||
|
source: local_id.clone(),
|
||||||
|
dest: peer_id.clone(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// query block state
|
||||||
|
profile.blocking = match sbot_client.friends_is_blocking(block_query).await {
|
||||||
|
Ok(blocking) if blocking == "true" => Some(true),
|
||||||
|
Ok(blocking) if blocking == "false" => Some(false),
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
|
||||||
|
peer_id
|
||||||
|
} else {
|
||||||
|
// if an ssb_id has not been provided, retrieve the local id using whoami
|
||||||
|
profile.is_local_profile = true;
|
||||||
|
|
||||||
|
local_id
|
||||||
|
};
|
||||||
|
|
||||||
|
// retrieve the profile info for the given id
|
||||||
|
let info = sbot_client.get_profile_info(&id).await?;
|
||||||
|
// set each profile field accordingly
|
||||||
|
for (key, val) in info {
|
||||||
|
match key.as_str() {
|
||||||
|
"name" => profile.name = Some(val),
|
||||||
|
"description" => profile.description = Some(val),
|
||||||
|
"image" => profile.image = Some(val),
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// assign the ssb public key
|
||||||
|
// (could be for the local profile or a peer)
|
||||||
|
profile.id = Some(id);
|
||||||
|
|
||||||
|
// determine the path to the blob defined by the value of `profile.image`
|
||||||
|
if let Some(ref blob_id) = profile.image {
|
||||||
|
profile.blob_path = match blobs::get_blob_path(&blob_id) {
|
||||||
|
Ok(path) => {
|
||||||
|
// if we get the path, check if the blob is in the blobstore.
|
||||||
|
// this allows us to default to a placeholder image in the template
|
||||||
|
if let Ok(exists) = blob_is_stored_locally(&path).await {
|
||||||
|
profile.blob_exists = exists
|
||||||
|
};
|
||||||
|
|
||||||
|
Some(path)
|
||||||
|
}
|
||||||
|
Err(_) => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(profile)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/// Retrieve a list of peers blocked by the local public key.
|
/// Retrieve a list of peers blocked by the local public key.
|
||||||
pub fn get_blocks_list() -> Result<Vec<HashMap<String, String>>, Box<dyn Error>> {
|
pub fn get_blocks_list() -> Result<Vec<HashMap<String, String>>, Box<dyn Error>> {
|
||||||
// retrieve latest go-sbot configuration parameters
|
// retrieve latest go-sbot configuration parameters
|
||||||
|
|
Loading…
Reference in New Issue