diff --git a/.gitignore b/.gitignore index d81f12e..71e1fc4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ /target /.idea +/.env +/src/sensitive.rs diff --git a/Cargo.lock b/Cargo.lock index a75ad98..f870fbc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -183,6 +183,7 @@ dependencies = [ "iana-time-zone", "js-sys", "num-traits", + "serde", "wasm-bindgen", "windows-targets 0.52.6", ] @@ -1440,12 +1441,15 @@ name = "toggl-2" version = "0.1.0" dependencies = [ "axum", + "base64", "chrono", "governor", "reqwest", "reqwest-middleware", "reqwest-ratelimit", "reqwest-retry", + "serde", + "serde_json", "thiserror", "tokio", ] diff --git a/Cargo.toml b/Cargo.toml index 5fb7ae4..3d30155 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,11 +5,14 @@ edition = "2021" [dependencies] axum = "0.7.5" -chrono = "0.4.38" +chrono = { version = "0.4.38", features = ["serde"] } governor = "0.6.3" -reqwest = "0.12.5" +reqwest = { version = "0.12.5", features = ["json"] } reqwest-ratelimit = "0.2.0" reqwest-middleware = "0.3" reqwest-retry = "0.6" thiserror = "1.0.62" tokio = { version = "1.38.0", features = ["full"] } +base64 = "0.22.1" +serde = { version = "1.0.204", features = ["derive"] } +serde_json = "1.0.120" diff --git a/src/main.rs b/src/main.rs index 7121248..95c4931 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,8 +1,14 @@ use toggl::TogglApi; mod toggl; +mod sensitive; #[tokio::main] async fn main() { - let api = TogglApi::new("api_key".to_string(), 123); + let api = TogglApi::new( + sensitive::API_TOKEN, + sensitive::WORKSPACE_ID, + ); + + dbg!(api.get_current_time_entry().await); } diff --git a/src/toggl/mod.rs b/src/toggl/mod.rs index d6b7bdd..12f211a 100644 --- a/src/toggl/mod.rs +++ b/src/toggl/mod.rs @@ -6,6 +6,9 @@ use std::num::NonZero; use axum::async_trait; use governor::state::{InMemoryState, NotKeyed}; use governor::clock::DefaultClock; +use reqwest::header::{HeaderMap, HeaderValue}; +use base64::engine::general_purpose::STANDARD; +use base64::Engine; struct ReqwestRateLimiter { rate_limiter: governor::RateLimiter, @@ -31,12 +34,15 @@ impl reqwest_ratelimit::RateLimiter for ReqwestRateLimiter { #[derive(Clone)] pub struct TogglApi { client: ClientWithMiddleware, - api_key: String, workspace_id: u32, + headers: HeaderMap, } +const BASE_URL: &str = "https://api.track.toggl.com/api/v9"; +const REPORTS_BASE_URL: &str = "https://api.track.toggl.com/reports/api/v3"; + impl TogglApi { - pub fn new(api_key: String, workspace_id: u32) -> Self { + pub fn new(api_key: &str, workspace_id: u32) -> Self { let rate_limiter = ReqwestRateLimiter::new(); let backoff = ExponentialBackoff::builder() .retry_bounds(Duration::from_secs(1), Duration::from_secs(60)) @@ -49,6 +55,79 @@ impl TogglApi { .with(RetryTransientMiddleware::new_with_policy(backoff)) .build(); - Self { client, api_key, workspace_id } + let toggl_auth = &STANDARD.encode(format!("{}:api_token", api_key)); + let headers = Self::authorisation_headers(toggl_auth); + + Self { client, workspace_id, headers } + } + + fn authorisation_headers(toggl_auth: &str) -> HeaderMap { + let mut headers = HeaderMap::new(); + let mut value = HeaderValue::from_str(&format!("Basic {}", toggl_auth)).unwrap(); + value.set_sensitive(true); + headers.insert("Authorization", value); + headers + } + + pub async fn get_current_time_entry(&self) -> Result, TogglError> { + let url = format!( + "{base_url}/me/time_entries/current", + base_url = BASE_URL + ); + + Ok(self.client.get(&url) + .headers(self.headers.clone()) + .send() + .await? + .json().await?) + } +} + +mod types { + use chrono::{DateTime, Utc}; + use serde::{Deserialize, Serialize}; + + #[derive(Serialize, Deserialize, Debug, Clone)] + pub struct TimeEntry { + id: u64, + workspace_id: u64, + user_id: u64, + project_id: Option, + task_id: Option, + + start: DateTime, + stop: Option>, + + // TODO This should be an Option as all negatives signify currently running time entries + duration: i32, + + at: DateTime, + + description: String, + + #[serde(default)] + tags: Vec, + + #[serde(default)] + tag_ids: Vec, + + billable: bool, + server_deleted_at: Option>, + permissions: Option, + } +} + +#[derive(Debug, thiserror::Error)] +pub enum TogglError { + #[error("Reqwest error: {0}")] + ReqwestError(#[from] reqwest_middleware::Error), + + #[error("Json error: {0}")] + JsonError(#[from] serde_json::Error), +} + +impl From for TogglError { + fn from(value: reqwest::Error) -> Self { + TogglError::ReqwestError(reqwest_middleware::Error::Reqwest(value)) } }