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::User;
10
11pub mod teams;
12pub mod users;
13pub mod website_stats;
14
15pub struct Umami {
16 pub client: Client,
17 pub token: Token,
18 pub instance: String,
19}
20
21#[derive(Serialize)]
22#[serde(rename_all = "camelCase")]
23pub struct Timestamps {
24 start_at: DateTime<Utc>,
25 end_at: DateTime<Utc>,
26}
27
28#[derive(Default, Iterable)]
30pub struct Filters {
31 path: Option<&'static str>,
33 referrer: Option<&'static str>,
35 title: Option<&'static str>,
37 query: Option<&'static str>,
39 browser: Option<&'static str>,
41 os: Option<&'static str>,
43 device: Option<&'static str>,
45 country: Option<&'static str>,
47 region: Option<&'static str>,
49 city: Option<&'static str>,
51 hostname: Option<&'static str>,
53 tag: Option<&'static str>,
55 segment: Option<&'static str>,
57 cohort: Option<&'static str>,
59}
60
61impl Filters {
62 fn as_parameters(&self) -> Vec<(&str, String)> {
64 self.iter()
65 .filter_map(|(key, value)| {
66 if let Some(Some(string)) = value.downcast_ref::<Option<&str>>() {
67 Some((key, string.to_string()))
68 } else {
69 None
70 }
71 }).collect()
72 }
73}
74
75#[derive(Clone, Debug, Deserialize)]
76pub struct Token {
77 pub token: String,
78 pub user: User,
79}
80
81impl Umami {
82 pub async fn new(instance: String, username: String, password: String) -> Result<Self, UmamiError> {
88 let client = Client::new();
89 let token = get_new_token(&instance, &username, &password).await?;
90
91 Ok(Self {
92 client,
93 token,
94 instance,
95 })
96 }
97
98 pub async fn request<T: DeserializeOwned, P: Serialize + Sized>(&self, method: &str, endpoint: &str, params: P) -> Result<T, UmamiError> {
99 let url = format!("{}/{}", self.instance, endpoint);
100
101 let request = match method {
102 "post" => self.client.post(&url).json(¶ms), _ => self.client.get(&url).query(¶ms),
104 };
105
106 let response = request
107 .bearer_auth(&self.token.token)
108 .send()
109 .await?;
110
111 let text = response
114 .text()
115 .await?;
116
117 Ok(serde_json::from_str::<T>(&text)?)
118 }
119}
120
121pub async fn get_new_token(instance: &str, username: &str, password: &str) -> Result<Token, UmamiError> {
123 let client = Client::new();
124 let url = format!("{}/auth/login", instance);
125
126 let mut params = HashMap::new();
127 params.insert("username", username);
128 params.insert("password", password);
129
130 let response = client
131 .post(&url)
132 .json(¶ms)
133 .send()
134 .await?;
135
136 let text = response
137 .text()
138 .await?;
139
140 Ok(serde_json::from_str::<Token>(&text)?)
141}
142
143#[derive(Error, Debug)]
145pub enum UmamiError {
146 #[error("HTTP request failed: {0}")]
148 RequestFailed(#[from] reqwest::Error),
149 #[error("Failed to parse JSON: {0}")]
151 JsonParseError(#[from] serde_json::Error),
152}