Initial impl

This commit is contained in:
2024-07-15 15:59:30 +01:00
parent bd5608a1de
commit 5539b7706d
5 changed files with 100 additions and 6 deletions
+7 -1
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);
}
+82 -3
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))
}
}