Adds gotify notifications

This commit is contained in:
2025-10-09 14:25:08 +02:00
parent caf98654ce
commit 0717f1694f
6 changed files with 909 additions and 29 deletions

828
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -12,3 +12,4 @@ diesel = { version = "2.3.2", features = ["postgres", "uuid", "chrono"] }
chrono = "0.4.42" chrono = "0.4.42"
log = "0.4.28" log = "0.4.28"
fern = "0.7.1" fern = "0.7.1"
reqwest = { version = "0.12.23", features = ["json"] }

View File

@@ -1,9 +1,10 @@
use crate::models::AppState; use crate::models::AppState;
use crate::models::alert::Alert;
use crate::models::hit::Hit; use crate::models::hit::Hit;
use crate::models::tracker::Tracker; use crate::models::tracker::Tracker;
use crate::schema::{hits, trackers}; use crate::schema::{hits, trackers};
use chrono::Utc; use chrono::Utc;
use diesel::{ExpressionMethods, QueryDsl, RunQueryDsl}; use diesel::{ExpressionMethods, QueryDsl, QueryResult, RunQueryDsl};
use rocket::fs::NamedFile; use rocket::fs::NamedFile;
use rocket::http::Status; use rocket::http::Status;
use rocket::request::{FromRequest, Outcome}; use rocket::request::{FromRequest, Outcome};
@@ -51,33 +52,76 @@ pub async fn get(id: &str, meta: ReqMeta, state: &State<AppState>) -> Result<Nam
None => return Err(Status::BadRequest), None => return Err(Status::BadRequest),
}; };
let mut tracker: Option<Tracker> = None;
let mut result: Option<QueryResult<usize>> = None;
let now = Utc::now().naive_utc();
{ {
let mut db = state.db.lock().unwrap(); let mut db = state.db.lock().unwrap();
let result = trackers::dsl::trackers let tresult = trackers::dsl::trackers
.filter(trackers::id.eq(id)) .filter(trackers::id.eq(id))
.first::<Tracker>(&mut *db) .first::<Tracker>(&mut *db)
.ok(); .ok();
if result.is_none() { if tresult.is_none() {
return Err(Status::NotFound); return Err(Status::NotFound);
} }
tracker = Some(tresult.unwrap());
let result = diesel::insert_into(hits::table) result = Some(
diesel::insert_into(hits::table)
.values(&Hit { .values(&Hit {
id: Uuid::new_v4(), id: Uuid::new_v4(),
tracker_id: id, tracker_id: id,
ip: meta.ip, ip: meta.ip.clone(),
agent: meta.agent, agent: meta.agent.clone(),
language: meta.language, language: meta.language.clone(),
created_at: Utc::now().naive_utc(), created_at: now,
}) })
.execute(&mut *db); .execute(&mut *db),
if result.is_err() { );
if result.as_ref().unwrap().is_err() {
error!("Failed to insert hit: {:?}", result); error!("Failed to insert hit: {:?}", result);
} }
} }
let url = state.gotify.url.clone();
let token = state.gotify.token.clone();
let tracker = tracker.unwrap();
let tracker_name = tracker.name.clone().unwrap_or("unknown".to_string());
let tracker_id = tracker.id.clone();
if state.gotify.enabled {
let alert = Alert {
title: format!("Tracker '{tracker_name}' ({tracker_id}) has been hit"),
message: format!(
"IP: {}\nAgent: {}\nLanguage: {}\nCreated at: {}\nError: {}",
meta.ip,
meta.agent.unwrap_or("unknown".to_string()),
meta.language.unwrap_or("unknown".to_string()),
now.to_string(),
result
.unwrap()
.err()
.map(|e| e.to_string())
.unwrap_or_else(|| "none".to_string()),
),
priority: 5,
};
let client = reqwest::Client::new();
let result = client
.post(format!("{url}/message?token={token}"))
.json(&alert)
.send()
.await;
if result.is_err() {
error!("Failed to send a alert to {url}: {result:?}");
}
}
NamedFile::open(Path::new(state.static_dir.as_str()).join("image.png")) NamedFile::open(Path::new(state.static_dir.as_str()).join("image.png"))
.await .await
.map_err(|_| Status::InternalServerError) .map_err(|_| Status::InternalServerError)

View File

@@ -7,7 +7,7 @@ mod schema;
use crate::api::hit; use crate::api::hit;
use crate::api::image; use crate::api::image;
use crate::api::tracker; use crate::api::tracker;
use crate::models::AppState; use crate::models::{AppState, GotifyState};
use chrono::Local; use chrono::Local;
use diesel::{Connection, PgConnection}; use diesel::{Connection, PgConnection};
use rocket::State; use rocket::State;
@@ -55,10 +55,24 @@ fn rocket() -> _ {
let database_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set"); let database_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set");
let static_dir = env::var("STATIC_DIR").expect("STATIC_DIR must be set"); let static_dir = env::var("STATIC_DIR").expect("STATIC_DIR must be set");
let gotify_url = env::var("GOTIFY_URL").unwrap_or("".to_string());
let gotify_token = env::var("GOTIFY_TOKEN").unwrap_or("".to_string());
let gotify_enabled_raw = env::var("GOTIFY_ENABLED").unwrap_or("false".to_string());
let gotify_enabled = gotify_enabled_raw.to_lowercase().trim() == "true";
if (gotify_url.is_empty() || gotify_token.is_empty()) && gotify_enabled {
panic!("Gotify is enabled but either GOTIFY_URL or GOTIFY_TOKEN isn't set");
}
let db = PgConnection::establish(&database_url) let db = PgConnection::establish(&database_url)
.expect(&format!("Error connecting to {}", database_url)); .expect(&format!("Error connecting to {}", database_url));
let app_data = AppState::new(db, static_dir); let gotify_data = GotifyState {
url: gotify_url,
token: gotify_token,
enabled: gotify_enabled
};
let app_data = AppState::new(db, static_dir, gotify_data);
rocket::build() rocket::build()
.manage(app_data) .manage(app_data)
.mount("/", routes![index]) .mount("/", routes![index])

8
src/models/alert.rs Normal file
View File

@@ -0,0 +1,8 @@
use serde::Serialize;
#[derive(Serialize)]
pub struct Alert {
pub title: String,
pub message: String,
pub priority: i32,
}

View File

@@ -1,19 +1,28 @@
use diesel::PgConnection; use diesel::PgConnection;
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
pub mod alert;
pub mod hit; pub mod hit;
pub mod tracker; pub mod tracker;
pub struct GotifyState {
pub url: String,
pub token: String,
pub enabled: bool,
}
pub struct AppState { pub struct AppState {
pub db: Arc<Mutex<PgConnection>>, pub db: Arc<Mutex<PgConnection>>,
pub static_dir: String, pub static_dir: String,
pub gotify: GotifyState,
} }
impl AppState { impl AppState {
pub fn new(db: PgConnection, static_dir: String) -> Self { pub fn new(db: PgConnection, static_dir: String, gotify: GotifyState) -> Self {
AppState { AppState {
db: Arc::new(Mutex::new(db)), db: Arc::new(Mutex::new(db)),
static_dir, static_dir,
gotify,
} }
} }
} }