add optional umami tracking

This commit is contained in:
Chris W 2023-10-09 13:09:11 -06:00
parent 6e795858bf
commit f9a76b4cef
3 changed files with 88 additions and 7 deletions

13
Cargo.lock generated
View File

@ -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"

View File

@ -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"
reqwest = "0.11.22"
umami_metrics = "0.1.0"

View File

@ -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<Umami> = 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<rgba::Rgba, Error> {
}
#[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<String> = 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<String> = languages.drain(..).collect();
let mut unique_languages: Vec<String> = 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<config::ConfigQuery>) -> impl Responder {
async fn generate(request: HttpRequest, info: web::Query<config::ConfigQuery>) -> 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<config::ConfigQuery>) -> 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"))