diff --git a/Cargo.toml b/Cargo.toml index 018dbd1..ce7ab25 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,11 +15,13 @@ path = "src/bin/main.rs" name = "route96" [features] -default = ["nip96", "blossom"] +default = ["nip96", "blossom", "analytics"] labels = ["nip96", "dep:candle-core", "dep:candle-nn", "dep:candle-transformers"] nip96 = ["dep:ffmpeg-sys-the-third", "dep:blurhash", "dep:libc"] blossom = [] bin-void-cat-migrate = ["dep:sqlx-postgres", "dep:clap", "dep:clap_derive"] +torrent-v2 = [] +analytics = [] [dependencies] log = "0.4.21" diff --git a/config.toml b/config.toml index 9ea84e3..cef8089 100644 --- a/config.toml +++ b/config.toml @@ -20,4 +20,7 @@ public_url = "http://localhost:8000" # vit_model_path = "model.safetennsors" # Webhook api endpoint -# webhook_url = "https://api.snort.social/api/v1/media/webhook" \ No newline at end of file +# webhook_url = "https://api.snort.social/api/v1/media/webhook" + +# Analytics support +# plausible_url = "https://plausible.com/" \ No newline at end of file diff --git a/src/analytics/mod.rs b/src/analytics/mod.rs new file mode 100644 index 0000000..d7d7dfb --- /dev/null +++ b/src/analytics/mod.rs @@ -0,0 +1,41 @@ +use anyhow::Error; +use log::warn; +use rocket::fairing::{Fairing, Info, Kind}; +use rocket::{Data, Request}; + +pub mod plausible; + +pub trait Analytics { + fn track(&self, req: &Request) -> Result<(), Error>; +} + +pub struct AnalyticsFairing { + inner: Box, +} + +impl AnalyticsFairing { + pub fn new(inner: T) -> Self + where + T: Analytics + Send + Sync + 'static, + { + Self { + inner: Box::new(inner), + } + } +} + +#[rocket::async_trait] +impl Fairing for AnalyticsFairing { + fn info(&self) -> Info { + Info { + name: "Analytics", + kind: Kind::Request, + } + } + + async fn on_request(&self, req: &mut Request<'_>, _data: &mut Data<'_>) { + if let Err(e) = self.inner.track(req) { + warn!("Failed to track! {}", e); + } + } +} diff --git a/src/analytics/plausible.rs b/src/analytics/plausible.rs new file mode 100644 index 0000000..6e1ce44 --- /dev/null +++ b/src/analytics/plausible.rs @@ -0,0 +1,55 @@ +use crate::analytics::Analytics; +use crate::settings::Settings; +use anyhow::Error; +use log::{info, warn}; +use rocket::Request; +use serde::{Deserialize, Serialize}; +use tokio::sync::mpsc::{unbounded_channel, UnboundedSender}; + +#[derive(Debug, Serialize, Deserialize)] +struct Event { + pub name: String, + pub domain: String, + pub url: String, + pub referrer: Option, +} + +pub struct PlausibleAnalytics { + tx: UnboundedSender, +} + +impl PlausibleAnalytics { + pub fn new(settings: &Settings) -> Self { + let (tx, mut rx) = unbounded_channel::(); + let url = match &settings.plausible_url { + Some(s) => s.clone(), + _ => "".to_string(), + }; + let pub_url = settings.public_url.clone(); + tokio::spawn(async move { + while let Some(mut msg) = rx.recv().await { + msg.url = format!("{}{}", pub_url, msg.url); + match ureq::post(&format!("{}/api/event", url)).send_json(&msg) { + Ok(v) => info!("Sent {:?}", msg), + Err(e) => warn!("Failed to track: {}", e), + } + } + }); + + Self { tx } + } +} + +impl Analytics for PlausibleAnalytics { + fn track(&self, req: &Request) -> Result<(), Error> { + Ok(self.tx.send(Event { + name: "pageview".to_string(), + domain: match req.host() { + Some(s) => s.to_string(), + None => return Ok(()), // ignore request + }, + url: req.uri().to_string(), + referrer: None, + })?) + } +} diff --git a/src/bin/main.rs b/src/bin/main.rs index 6fed642..34e788e 100644 --- a/src/bin/main.rs +++ b/src/bin/main.rs @@ -7,7 +7,10 @@ use rocket::config::Ident; use rocket::data::{ByteUnit, Limits}; use rocket::routes; use rocket::shield::Shield; - +#[cfg(feature = "analytics")] +use route96::analytics::plausible::PlausibleAnalytics; +#[cfg(feature = "analytics")] +use route96::analytics::AnalyticsFairing; use route96::cors::CORS; use route96::db::Database; use route96::filesystem::FileStore; @@ -61,6 +64,12 @@ async fn main() -> Result<(), Error> { .attach(Shield::new()) // disable .mount("/", routes![root, get_blob, head_blob]); + #[cfg(feature = "analytics")] + { + if settings.plausible_url.is_some() { + rocket = rocket.attach(AnalyticsFairing::new(PlausibleAnalytics::new(&settings))) + } + } #[cfg(feature = "blossom")] { rocket = rocket.mount("/", routes::blossom_routes()); diff --git a/src/lib.rs b/src/lib.rs index 73fac13..493254e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -7,4 +7,6 @@ pub mod filesystem; pub mod processing; pub mod routes; pub mod settings; -pub mod webhook; \ No newline at end of file +pub mod webhook; +#[cfg(feature = "analytics")] +pub mod analytics; \ No newline at end of file diff --git a/src/settings.rs b/src/settings.rs index 8a242c8..454edf2 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -1,5 +1,5 @@ -use std::path::PathBuf; use serde::{Deserialize, Serialize}; +use std::path::PathBuf; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Settings { @@ -20,10 +20,13 @@ pub struct Settings { /// Whitelisted pubkeys pub whitelist: Option>, - + /// Path for ViT image model pub vit_model_path: Option, - + /// Webhook api endpoint pub webhook_url: Option, + + /// Analytics tracking + pub plausible_url: Option, }