1use std::collections::HashMap;
2
3use chrono::{DateTime, Utc};
4use reqwest::Client;
5use serde::{Deserialize, Serialize, de::DeserializeOwned};
6use struct_iterable::Iterable;
7use thiserror::Error;
8
9use crate::users::UserWithTeams;
10
11pub mod admin;
12pub mod teams;
13pub mod users;
14pub mod website_stats;
15pub mod websites;
16
17pub struct Umami {
18 pub client: Client,
19 pub token: Token,
20 pub instance: String,
21}
22
23#[derive(Serialize)]
25#[serde(rename_all = "camelCase")]
26pub struct Timestamps {
27 pub start_at: DateTime<Utc>,
28 pub end_at: DateTime<Utc>,
29}
30
31#[derive(Default, Iterable)]
33pub struct Filters {
34 pub path: Option<&'static str>,
36 pub referrer: Option<&'static str>,
38 pub title: Option<&'static str>,
40 pub query: Option<&'static str>,
42 pub browser: Option<&'static str>,
44 pub os: Option<&'static str>,
46 pub device: Option<&'static str>,
48 pub country: Option<&'static str>,
50 pub region: Option<&'static str>,
52 pub city: Option<&'static str>,
54 pub hostname: Option<&'static str>,
56 pub tag: Option<&'static str>,
58 pub segment: Option<&'static str>,
60 pub cohort: Option<&'static str>,
62}
63
64impl Filters {
65 fn as_parameters(&self) -> Vec<(&str, String)> {
67 self.iter()
68 .filter_map(|(key, value)| {
69 if let Some(Some(string)) = value.downcast_ref::<Option<&str>>() {
70 Some((key, string.to_string()))
71 } else {
72 None
73 }
74 }).collect()
75 }
76}
77
78#[derive(Clone, Debug, Deserialize)]
79pub struct Token {
80 pub token: String,
81 pub user: UserWithTeams,
82}
83
84impl Umami {
85 pub async fn new(instance: String, username: String, password: String) -> Result<Self, UmamiError> {
91 let client = Client::new();
92 let token = get_new_token(&instance, &username, &password).await?;
93
94 Ok(Self {
95 client,
96 token,
97 instance,
98 })
99 }
100
101 pub async fn request<T: DeserializeOwned, P: Serialize + Sized>(&self, method: &str, endpoint: &str, params: P) -> Result<T, UmamiError> {
102 let url = format!("{}/{}", self.instance, endpoint);
103
104 let request = match method {
105 "post" => self.client.post(&url).json(¶ms), _ => self.client.get(&url).query(¶ms),
107 };
108
109 let response = request
110 .bearer_auth(&self.token.token)
111 .send()
112 .await?;
113
114 let text = response
117 .text()
118 .await?;
119
120 Ok(serde_json::from_str::<T>(&text)?)
121 }
122}
123
124pub async fn get_new_token(instance: &str, username: &str, password: &str) -> Result<Token, UmamiError> {
126 let client = Client::new();
127 let url = format!("{}/auth/login", instance);
128
129 let mut params = HashMap::new();
130 params.insert("username", username);
131 params.insert("password", password);
132
133 let response = client
134 .post(&url)
135 .json(¶ms)
136 .send()
137 .await?;
138
139 let text = response
140 .text()
141 .await?;
142
143 Ok(serde_json::from_str::<Token>(&text)?)
144}
145
146#[derive(Error, Debug)]
148pub enum UmamiError {
149 #[error("HTTP request failed: {0}")]
151 RequestFailed(#[from] reqwest::Error),
152 #[error("Failed to parse JSON: {0}")]
154 JsonParseError(#[from] serde_json::Error),
155}