Compare commits

...

2 Commits

7 changed files with 150 additions and 15 deletions
Generated
+16 -1
View File
@@ -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"
+1
View File
@@ -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" }
+15 -11
View File
@@ -4,6 +4,7 @@ use reqwest::Error as ReqwestError;
use crate::country_statistics::country_nodes_distribution::CountryNodesDistribution;
use crate::mix_nodes::{GeoLocation, Location};
use crate::state::ExplorerApiStateContext;
use std::env;
pub mod country_nodes_distribution;
pub mod http;
@@ -18,18 +19,21 @@ impl CountryStatistics {
}
pub(crate) fn start(mut self) {
info!("Spawning task runner...");
tokio::spawn(async move {
let mut interval_timer = tokio::time::interval(std::time::Duration::from_secs(60 * 60));
loop {
// wait for the next interval tick
interval_timer.tick().await;
if env::var("DEV_MODE").is_err() {
info!("Spawning task runner...");
tokio::spawn(async move {
let mut interval_timer =
tokio::time::interval(std::time::Duration::from_secs(60 * 60));
loop {
// wait for the next interval tick
interval_timer.tick().await;
info!("Running task...");
self.calculate_nodes_per_country().await;
info!("Done");
}
});
info!("Running task...");
self.calculate_nodes_per_country().await;
info!("Done");
}
});
}
}
/// Retrieves the current list of mixnodes from the validators and calculates how many nodes are in each country
+4
View File
@@ -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
+40 -3
View File
@@ -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<Route> {
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("/<pubkey>/preview")]
pub(crate) async fn preview(
pubkey: &str,
templates: &State<Templates>,
state: &State<ExplorerApiStateContext>,
) -> Html<String> {
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&amp;rid=200.gif&amp;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("/<pubkey>/delegations")]
pub(crate) async fn get_delegations(pubkey: &str) -> Json<Vec<mixnet_contract::Delegation>> {
@@ -63,6 +93,13 @@ pub(crate) async fn get_description(
pubkey: &str,
state: &State<ExplorerApiStateContext>,
) -> Option<Json<NodeDescription>> {
get_mixnode_description(pubkey, state).await.map(Json)
}
async fn get_mixnode_description(
pubkey: &str,
state: &State<ExplorerApiStateContext>,
) -> Option<NodeDescription> {
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!(
+1
View File
@@ -1,3 +1,4 @@
mod cache;
pub(crate) mod http;
pub(crate) mod models;
pub(crate) mod templates;
+73
View File
@@ -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<String, RenderError> {
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#"<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>{{ title }}</title>
<meta name="description" content="{{ description }}">
<meta property="og:type" content="article">
<meta property="og:url" content="{{ url }}">
<meta property="og:title" content="{{ title }}">
<meta property="og:description" content="{{ description }}">
<meta property="og:image" content="{{ image_url }}">
<meta name="twitter:card" value="summary_large_image">
<meta name="twitter:title" value="{{ title }}">
<meta name="twitter:description" value="{{ description }}">
<meta name="twitter:image" value="{{ image_url }}">
<meta name="twitter:site" value="@nymtech">
<meta http-equiv="refresh" content="0;url={{url}}" />
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
}
.meme {
padding-top: 20px;
}
.meme > img {
max-width: 200px;
max-height: 200px;
}
</style>
</head>
<body>
<h1>{{title}}</h1>
<div>{{description}}<div>
</body>
</html>"#;