diff --git a/Cargo.lock b/Cargo.lock index e13f354df6..aeb3d472af 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -766,7 +766,7 @@ dependencies = [ name = "config" version = "0.1.0" dependencies = [ - "handlebars", + "handlebars 3.5.5", "humantime-serde", "network-defaults", "serde", @@ -1582,6 +1582,7 @@ name = "explorer-api" version = "0.1.0" dependencies = [ "chrono", + "handlebars 4.1.3", "humantime-serde", "isocountry", "log", @@ -2227,6 +2228,20 @@ dependencies = [ "serde_json", ] +[[package]] +name = "handlebars" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66b09e2322d20d14bc2572401ce7c1d60b4748580a76c230ed9c1f8938f0c833" +dependencies = [ + "log", + "pest", + "pest_derive", + "quick-error 2.0.1", + "serde", + "serde_json", +] + [[package]] name = "hashbrown" version = "0.11.2" diff --git a/explorer-api/Cargo.toml b/explorer-api/Cargo.toml index cf97221de4..d347602d6e 100644 --- a/explorer-api/Cargo.toml +++ b/explorer-api/Cargo.toml @@ -20,6 +20,7 @@ okapi = { version = "0.6.0-alpha-1", features = ["derive_json_schema"] } rocket_okapi = "0.7.0-alpha-1" log = "0.4.0" pretty_env_logger = "0.4.0" +handlebars = "4.1.3" mixnet-contract = { path = "../common/mixnet-contract" } network-defaults = { path = "../common/network-defaults" } diff --git a/explorer-api/src/http/mod.rs b/explorer-api/src/http/mod.rs index 2a3171081d..291c56cac3 100644 --- a/explorer-api/src/http/mod.rs +++ b/explorer-api/src/http/mod.rs @@ -7,6 +7,7 @@ use rocket_okapi::swagger_ui::make_swagger_ui; use crate::country_statistics::http::country_statistics_make_default_routes; use crate::http::swagger::get_docs; use crate::mix_node::http::mix_node_make_default_routes; +use crate::mix_node::templates::Templates; use crate::ping::http::ping_make_default_routes; use crate::state::ExplorerApiStateContext; @@ -29,6 +30,8 @@ pub(crate) fn start(state: ExplorerApiStateContext) { .to_cors() .unwrap(); + let templates = Templates::new(); + let config = rocket::config::Config::release_default(); rocket::build() .configure(config) @@ -38,6 +41,7 @@ pub(crate) fn start(state: ExplorerApiStateContext) { .mount("/swagger", make_swagger_ui(&get_docs())) .register("/", catchers![not_found]) .manage(state) + .manage(templates) .attach(cors) .launch() .await diff --git a/explorer-api/src/mix_node/http.rs b/explorer-api/src/mix_node/http.rs index 56cb5ba848..4849821dcd 100644 --- a/explorer-api/src/mix_node/http.rs +++ b/explorer-api/src/mix_node/http.rs @@ -1,4 +1,5 @@ use reqwest::Error as ReqwestError; +use rocket::response::content::Html; use rocket::serde::json::Json; use rocket::{Route, State}; use serde::Serialize; @@ -6,11 +7,12 @@ use serde::Serialize; use mixnet_contract::{Addr, Coin, Layer, MixNode}; use crate::mix_node::models::{NodeDescription, NodeStats}; +use crate::mix_node::templates::{PreviewTemplateData, Templates}; use crate::mix_nodes::{get_mixnode_delegations, Location}; use crate::state::ExplorerApiStateContext; pub fn mix_node_make_default_routes() -> Vec { - routes_with_openapi![get_delegations, get_description, get_stats, list] + routes_with_openapi![get_delegations, get_description, get_stats, list, preview] } #[derive(Clone, Debug, Serialize, JsonSchema)] @@ -51,6 +53,34 @@ pub(crate) async fn list( ) } +#[openapi(tag = "mix_node")] +#[get("//preview")] +pub(crate) async fn preview( + pubkey: &str, + templates: &State, + state: &State, +) -> Html { + match get_mixnode_description(pubkey, state).await { + Some(node_description) => { + // use handlebars to render an HTML output for an OpenGraph / Twitter preview - this is + // used in social media apps / messenger apps that show previews for links + match templates.render_preview(PreviewTemplateData { + title: node_description.name, + description: node_description.description, + url: format!( + "https://testnet-milhon-explorer.nymtech.net/nym/mixnodes/{}", + pubkey + ), + image_url: String::from("https://media2.giphy.com/media/pwyW4XDmtqjG8/200.gif?cid=dda24d50ae15e44c38783edc824618df68645c6af2592b28&rid=200.gif&ct=g"), + }) { + Ok(r) => Html(r), + Err(_e) => Html(String::from("Oh no, something went wrong!")), + } + } + None => Html(String::from("Sorry, mix node not found")), + } +} + #[openapi(tag = "mix_node")] #[get("//delegations")] pub(crate) async fn get_delegations(pubkey: &str) -> Json> { @@ -63,6 +93,13 @@ pub(crate) async fn get_description( pubkey: &str, state: &State, ) -> Option> { + get_mixnode_description(pubkey, state).await.map(Json) +} + +async fn get_mixnode_description( + pubkey: &str, + state: &State, +) -> Option { match state .inner .mix_node_cache @@ -72,7 +109,7 @@ pub(crate) async fn get_description( { Some(cache_value) => { trace!("Returning cached value for {}", pubkey); - Some(Json(cache_value)) + Some(cache_value) } None => { trace!("No valid cache value for {}", pubkey); @@ -91,7 +128,7 @@ pub(crate) async fn get_description( .mix_node_cache .set_description(pubkey, response.clone()) .await; - Some(Json(response)) + Some(response) } Err(e) => { error!( diff --git a/explorer-api/src/mix_node/mod.rs b/explorer-api/src/mix_node/mod.rs index b57132ec2c..3bdeb52065 100644 --- a/explorer-api/src/mix_node/mod.rs +++ b/explorer-api/src/mix_node/mod.rs @@ -1,3 +1,4 @@ mod cache; pub(crate) mod http; pub(crate) mod models; +pub(crate) mod templates; diff --git a/explorer-api/src/mix_node/templates.rs b/explorer-api/src/mix_node/templates.rs new file mode 100644 index 0000000000..d543f92c68 --- /dev/null +++ b/explorer-api/src/mix_node/templates.rs @@ -0,0 +1,73 @@ +use handlebars::{Handlebars, RenderError}; +use serde::Serialize; + +#[derive(Clone)] +pub(crate) struct Templates { + handlebars: Handlebars<'static>, +} + +impl Templates { + pub(crate) fn new() -> Self { + let mut handlebars = Handlebars::new(); + + assert!(handlebars + .register_template_string("preview", PREVIEW_TEMPLATE) + .is_ok()); + + Templates { handlebars } + } + + pub(crate) fn render_preview(&self, data: PreviewTemplateData) -> Result { + self.handlebars.render("preview", &data) + } +} + +#[derive(Clone, Debug, Serialize, JsonSchema)] +pub(crate) struct PreviewTemplateData { + pub(crate) title: String, + pub(crate) description: String, + pub(crate) url: String, + pub(crate) image_url: String, +} + +const PREVIEW_TEMPLATE: &str = r#" + + + + + + {{ title }} + + + + + + + + + + + + + + + + + + + +

{{title}}

+
{{description}}
+ +"#;