diff --git a/Cargo.lock b/Cargo.lock index b484933..9b60c57 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1246,6 +1246,7 @@ dependencies = [ "structopt", "syntect", "thiserror", + "umami_metrics", ] [[package]] @@ -2636,6 +2637,18 @@ version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" +[[package]] +name = "umami_metrics" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbc9ec451bb0504e32cafb076fe46e0126c70ad167846e3de02f0a2bbebc6839" +dependencies = [ + "anyhow", + "reqwest", + "serde", + "serde_json", +] + [[package]] name = "unicode-bidi" version = "0.3.13" diff --git a/Cargo.toml b/Cargo.toml index 328676c..94bf932 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,4 +16,5 @@ anyhow = "1.0.75" thiserror = "1.0.49" syntect = "5.1.0" font-kit = "0.11.0" -reqwest = "0.11.22" \ No newline at end of file +reqwest = "0.11.22" +umami_metrics = "0.1.0" \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index ced48b2..4925ea9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,9 +1,10 @@ #[macro_use] extern crate anyhow; -use actix_web::{get, web, App, HttpResponse, HttpServer, Responder}; +use actix_web::{get, web, App, HttpResponse, HttpServer, Responder, HttpRequest}; use anyhow::Error; use lazy_static::lazy_static; +use reqwest::header::HeaderValue; use silicon as si; use silicon::utils::ToRgba; use std::collections::HashSet; @@ -11,6 +12,7 @@ use std::io::Cursor; use std::num::ParseIntError; use syntect::easy::HighlightLines; use syntect::util::LinesWithEndings; +use umami_metrics::Umami; mod config; mod rgba; @@ -20,6 +22,13 @@ lazy_static! { silicon::assets::HighlightingAssets::new(); } +lazy_static! { + static ref UMAMI: Option = match (std::env::var("UMAMI_WEBSITE_ID"), std::env::var("UMAMI_URL")) { + (Ok(website_id), Ok(url)) => Some(Umami::new(website_id, url)), + _ => None, + }; +} + macro_rules! unwrap_or_return { ( $e:expr, $r:expr ) => { match $e { @@ -29,6 +38,33 @@ macro_rules! unwrap_or_return { }; } +async fn pageview(path: &str, request: &HttpRequest) { + let um = &*UMAMI; + match um { + Some(um) => { + let referrer = request.headers().get("Referer").unwrap_or(&HeaderValue::from_static("")).to_str().unwrap().to_owned(); + let hostname = request.headers().get("Host").unwrap_or(&HeaderValue::from_static("")).to_str().unwrap().to_owned(); + let language = request.headers().get("Accept-Language").unwrap_or(&HeaderValue::from_static("")).to_str().unwrap().to_owned(); + let screen = request.headers().get("Screen").unwrap_or(&HeaderValue::from_static("")).to_str().unwrap().to_owned(); + _ = um.pageview(path.to_owned(), referrer, hostname, language, screen).await; + }, + None => (), + } +} + +async fn event(path: &str, event_type: &str, event_value: &str, request: &HttpRequest) { + let um = &*UMAMI; + match um { + Some(um) => { + let hostname = request.headers().get("Host").unwrap_or(&HeaderValue::from_static("")).to_str().unwrap().to_owned(); + let language = request.headers().get("Accept-Language").unwrap_or(&HeaderValue::from_static("")).to_str().unwrap().to_owned(); + let screen = request.headers().get("Screen").unwrap_or(&HeaderValue::from_static("")).to_str().unwrap().to_owned(); + _ = um.event(path.to_owned(), event_type.to_owned(), event_value.to_owned(), hostname, language, screen).await; + }, + None => (), + } +} + fn parse_font_str(s: &str) -> Vec<(String, f32)> { let mut result = vec![]; for font in s.split(';') { @@ -69,7 +105,7 @@ fn parse_str_color(s: &str) -> Result { } #[get("/")] -async fn help() -> impl Responder { +async fn help(request: HttpRequest) -> impl Responder { // Respond with some help text for how to use the API, // formatted as JSON since this is an API. let json = r#" @@ -110,21 +146,23 @@ async fn help() -> impl Responder { } "#; + pageview("/", &request).await; HttpResponse::Ok() .append_header(("Content-Type", "application/json")) .body(json) } #[get("/themes")] -async fn themes() -> impl Responder { +async fn themes(request: HttpRequest) -> impl Responder { let ha = &*HIGHLIGHTING_ASSETS; let themes = &ha.theme_set.themes; let theme_keys: Vec = themes.keys().map(|s| s.to_string()).collect(); + pageview("/themes", &request).await; HttpResponse::Ok().json(theme_keys) } #[get("/languages")] -async fn languages() -> impl Responder { +async fn languages(request: HttpRequest) -> impl Responder { let ha = &*HIGHLIGHTING_ASSETS; let syntaxes = &ha.syntax_set.syntaxes(); let mut languages = syntaxes @@ -134,19 +172,23 @@ async fn languages() -> impl Responder { let unique_languages: HashSet = languages.drain(..).collect(); let mut unique_languages: Vec = unique_languages.into_iter().collect(); unique_languages.sort(); + pageview("/languages", &request).await; HttpResponse::Ok().json(unique_languages) } #[get("/fonts")] -async fn fonts() -> impl Responder { +async fn fonts(request: HttpRequest) -> impl Responder { let source = font_kit::source::SystemSource::new(); let fonts = source.all_families().unwrap_or_default(); + pageview("/fonts", &request).await; HttpResponse::Ok().json(fonts) } #[get("/generate")] -async fn generate(info: web::Query) -> impl Responder { +async fn generate(request: HttpRequest, info: web::Query) -> impl Responder { let ha = &*HIGHLIGHTING_ASSETS; + pageview("/generate", &request).await; + let (ps, ts) = (&ha.syntax_set, &ha.theme_set); let mut conf = config::Config::default(); @@ -261,6 +303,31 @@ async fn generate(info: web::Query) -> impl Responder { .body(r#"{"error": "Failed to write image"}"#) ); + event("/generate", "generation", r#" + { + "code": "The code to generate an image from. Required.", + "language": "The language to use for syntax highlighting. Optional, will attempt to guess if not provided.", + "theme": "The theme to use for syntax highlighting. Optional, defaults to Dracula.", + "font": "The font to use. Optional, defaults to Fira Code.", + "shadow_color": "The color of the shadow. Optional, defaults to transparent.", + "background": "The background color. Optional, defaults to transparent.", + "tab_width": "The tab width. Optional, defaults to 4.", + "line_pad": "The line padding. Optional, defaults to 2.", + "line_offset": "The line offset. Optional, defaults to 1.", + "window_title": "The window title. Optional, defaults to \"Inkify\".", + "no_line_number": "Whether to hide the line numbers. Optional, defaults to false.", + "no_round_corner": "Whether to round the corners. Optional, defaults to false.", + "no_window_controls": "Whether to hide the window controls. Optional, defaults to false.", + "shadow_blur_radius": "The shadow blur radius. Optional, defaults to 0.", + "shadow_offset_x": "The shadow offset x. Optional, defaults to 0.", + "shadow_offset_y": "The shadow offset y. Optional, defaults to 0.", + "pad_horiz": "The horizontal padding. Optional, defaults to 80.", + "pad_vert": "The vertical padding. Optional, defaults to 100.", + "highlight_lines": "The lines to highlight. Optional, defaults to none.", + "background_image": "The background image for the padding area as a URL. Optional, defaults to none." + } + "#, &request).await; + // Return the image as a PNG. HttpResponse::Ok() .append_header(("Content-Type", "image/png"))