Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 61b58c2e5e | |||
| b2e3d5f3a3 |
Generated
+16
-1
@@ -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"
|
||||
|
||||
@@ -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" }
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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&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("/<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,3 +1,4 @@
|
||||
mod cache;
|
||||
pub(crate) mod http;
|
||||
pub(crate) mod models;
|
||||
pub(crate) mod templates;
|
||||
|
||||
@@ -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>"#;
|
||||
Reference in New Issue
Block a user