use std::collections::HashMap;
use chrono::{DateTime, Utc};
use reqwest::Client;
use serde::{Deserialize, Serialize, de::DeserializeOwned};
use struct_iterable::Iterable;
use thiserror::Error;
use crate::users::UserWithTeams;
pub mod admin;
pub mod teams;
pub mod users;
pub mod website_stats;
pub mod websites;
pub struct Umami {
pub client: Client,
pub token: Token,
pub instance: String,
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Timestamps {
pub start_at: DateTime<Utc>,
pub end_at: DateTime<Utc>,
}
#[derive(Default, Iterable)]
pub struct Filters {
pub path: Option<&'static str>,
pub referrer: Option<&'static str>,
pub title: Option<&'static str>,
pub query: Option<&'static str>,
pub browser: Option<&'static str>,
pub os: Option<&'static str>,
pub device: Option<&'static str>,
pub country: Option<&'static str>,
pub region: Option<&'static str>,
pub city: Option<&'static str>,
pub hostname: Option<&'static str>,
pub tag: Option<&'static str>,
pub segment: Option<&'static str>,
pub cohort: Option<&'static str>,
}
impl Filters {
fn as_parameters(&self) -> Vec<(&str, String)> {
self.iter()
.filter_map(|(key, value)| {
if let Some(Some(string)) = value.downcast_ref::<Option<&str>>() {
Some((key, string.to_string()))
} else {
None
}
}).collect()
}
}
#[derive(Clone, Debug, Deserialize)]
pub struct Token {
pub token: String,
pub user: UserWithTeams,
}
impl Umami {
pub async fn new(instance: String, username: String, password: String) -> Result<Self, UmamiError> {
let client = Client::new();
let token = get_new_token(&instance, &username, &password).await?;
Ok(Self {
client,
token,
instance,
})
}
pub async fn request<T: DeserializeOwned, P: Serialize + Sized>(&self, method: &str, endpoint: &str, params: P) -> Result<T, UmamiError> {
let url = format!("{}/{}", self.instance, endpoint);
let request = match method {
"post" => self.client.post(&url).json(¶ms), _ => self.client.get(&url).query(¶ms),
};
let response = request
.bearer_auth(&self.token.token)
.send()
.await?;
let text = response
.text()
.await?;
Ok(serde_json::from_str::<T>(&text)?)
}
}
pub async fn get_new_token(instance: &str, username: &str, password: &str) -> Result<Token, UmamiError> {
let client = Client::new();
let url = format!("{}/auth/login", instance);
let mut params = HashMap::new();
params.insert("username", username);
params.insert("password", password);
let response = client
.post(&url)
.json(¶ms)
.send()
.await?;
let text = response
.text()
.await?;
Ok(serde_json::from_str::<Token>(&text)?)
}
#[derive(Error, Debug)]
pub enum UmamiError {
#[error("HTTP request failed: {0}")]
RequestFailed(#[from] reqwest::Error),
#[error("Failed to parse JSON: {0}")]
JsonParseError(#[from] serde_json::Error),
}