Initial impl

This commit is contained in:
Joshua Coles 2024-07-15 15:59:30 +01:00
parent bd5608a1de
commit 5539b7706d
5 changed files with 100 additions and 6 deletions

2
.gitignore vendored
View File

@ -1,2 +1,4 @@
/target
/.idea
/.env
/src/sensitive.rs

4
Cargo.lock generated
View File

@ -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",
]

View File

@ -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"

View File

@ -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);
}

View File

@ -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<NotKeyed, InMemoryState, DefaultClock>,
@ -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<Option<types::TimeEntry>, 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<u64>,
task_id: Option<u64>,
start: DateTime<Utc>,
stop: Option<DateTime<Utc>>,
// TODO This should be an Option<u32> as all negatives signify currently running time entries
duration: i32,
at: DateTime<Utc>,
description: String,
#[serde(default)]
tags: Vec<String>,
#[serde(default)]
tag_ids: Vec<u64>,
billable: bool,
server_deleted_at: Option<DateTime<Utc>>,
permissions: Option<String>,
}
}
#[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<reqwest::Error> for TogglError {
fn from(value: reqwest::Error) -> Self {
TogglError::ReqwestError(reqwest_middleware::Error::Reqwest(value))
}
}