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"
log = "0.4.28"
fern = "0.7.1"
reqwest = { version = "0.12.23", features = ["json"] }

View File

@@ -1,9 +1,10 @@
use crate::models::AppState;
use crate::models::alert::Alert;
use crate::models::hit::Hit;
use crate::models::tracker::Tracker;
use crate::schema::{hits, trackers};
use chrono::Utc;
use diesel::{ExpressionMethods, QueryDsl, RunQueryDsl};
use diesel::{ExpressionMethods, QueryDsl, QueryResult, RunQueryDsl};
use rocket::fs::NamedFile;
use rocket::http::Status;
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),
};
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 result = trackers::dsl::trackers
let tresult = trackers::dsl::trackers
.filter(trackers::id.eq(id))
.first::<Tracker>(&mut *db)
.ok();
if result.is_none() {
if tresult.is_none() {
return Err(Status::NotFound);
}
tracker = Some(tresult.unwrap());
let result = diesel::insert_into(hits::table)
.values(&Hit {
id: Uuid::new_v4(),
tracker_id: id,
ip: meta.ip,
agent: meta.agent,
language: meta.language,
created_at: Utc::now().naive_utc(),
})
.execute(&mut *db);
if result.is_err() {
result = Some(
diesel::insert_into(hits::table)
.values(&Hit {
id: Uuid::new_v4(),
tracker_id: id,
ip: meta.ip.clone(),
agent: meta.agent.clone(),
language: meta.language.clone(),
created_at: now,
})
.execute(&mut *db),
);
if result.as_ref().unwrap().is_err() {
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"))
.await
.map_err(|_| Status::InternalServerError)

View File

@@ -7,7 +7,7 @@ mod schema;
use crate::api::hit;
use crate::api::image;
use crate::api::tracker;
use crate::models::AppState;
use crate::models::{AppState, GotifyState};
use chrono::Local;
use diesel::{Connection, PgConnection};
use rocket::State;
@@ -55,10 +55,24 @@ fn rocket() -> _ {
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 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)
.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()
.manage(app_data)
.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 std::sync::{Arc, Mutex};
pub mod alert;
pub mod hit;
pub mod tracker;
pub struct GotifyState {
pub url: String,
pub token: String,
pub enabled: bool,
}
pub struct AppState {
pub db: Arc<Mutex<PgConnection>>,
pub static_dir: String,
pub gotify: GotifyState,
}
impl AppState {
pub fn new(db: PgConnection, static_dir: String) -> Self {
pub fn new(db: PgConnection, static_dir: String, gotify: GotifyState) -> Self {
AppState {
db: Arc::new(Mutex::new(db)),
static_dir,
gotify,
}
}
}