Initial impl
This commit is contained in:
parent
bd5608a1de
commit
5539b7706d
2
.gitignore
vendored
2
.gitignore
vendored
@ -1,2 +1,4 @@
|
|||||||
/target
|
/target
|
||||||
/.idea
|
/.idea
|
||||||
|
/.env
|
||||||
|
/src/sensitive.rs
|
||||||
|
|||||||
4
Cargo.lock
generated
4
Cargo.lock
generated
@ -183,6 +183,7 @@ dependencies = [
|
|||||||
"iana-time-zone",
|
"iana-time-zone",
|
||||||
"js-sys",
|
"js-sys",
|
||||||
"num-traits",
|
"num-traits",
|
||||||
|
"serde",
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
"windows-targets 0.52.6",
|
"windows-targets 0.52.6",
|
||||||
]
|
]
|
||||||
@ -1440,12 +1441,15 @@ name = "toggl-2"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"axum",
|
"axum",
|
||||||
|
"base64",
|
||||||
"chrono",
|
"chrono",
|
||||||
"governor",
|
"governor",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"reqwest-middleware",
|
"reqwest-middleware",
|
||||||
"reqwest-ratelimit",
|
"reqwest-ratelimit",
|
||||||
"reqwest-retry",
|
"reqwest-retry",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"tokio",
|
"tokio",
|
||||||
]
|
]
|
||||||
|
|||||||
@ -5,11 +5,14 @@ edition = "2021"
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
axum = "0.7.5"
|
axum = "0.7.5"
|
||||||
chrono = "0.4.38"
|
chrono = { version = "0.4.38", features = ["serde"] }
|
||||||
governor = "0.6.3"
|
governor = "0.6.3"
|
||||||
reqwest = "0.12.5"
|
reqwest = { version = "0.12.5", features = ["json"] }
|
||||||
reqwest-ratelimit = "0.2.0"
|
reqwest-ratelimit = "0.2.0"
|
||||||
reqwest-middleware = "0.3"
|
reqwest-middleware = "0.3"
|
||||||
reqwest-retry = "0.6"
|
reqwest-retry = "0.6"
|
||||||
thiserror = "1.0.62"
|
thiserror = "1.0.62"
|
||||||
tokio = { version = "1.38.0", features = ["full"] }
|
tokio = { version = "1.38.0", features = ["full"] }
|
||||||
|
base64 = "0.22.1"
|
||||||
|
serde = { version = "1.0.204", features = ["derive"] }
|
||||||
|
serde_json = "1.0.120"
|
||||||
|
|||||||
@ -1,8 +1,14 @@
|
|||||||
use toggl::TogglApi;
|
use toggl::TogglApi;
|
||||||
|
|
||||||
mod toggl;
|
mod toggl;
|
||||||
|
mod sensitive;
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn 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);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,6 +6,9 @@ use std::num::NonZero;
|
|||||||
use axum::async_trait;
|
use axum::async_trait;
|
||||||
use governor::state::{InMemoryState, NotKeyed};
|
use governor::state::{InMemoryState, NotKeyed};
|
||||||
use governor::clock::DefaultClock;
|
use governor::clock::DefaultClock;
|
||||||
|
use reqwest::header::{HeaderMap, HeaderValue};
|
||||||
|
use base64::engine::general_purpose::STANDARD;
|
||||||
|
use base64::Engine;
|
||||||
|
|
||||||
struct ReqwestRateLimiter {
|
struct ReqwestRateLimiter {
|
||||||
rate_limiter: governor::RateLimiter<NotKeyed, InMemoryState, DefaultClock>,
|
rate_limiter: governor::RateLimiter<NotKeyed, InMemoryState, DefaultClock>,
|
||||||
@ -31,12 +34,15 @@ impl reqwest_ratelimit::RateLimiter for ReqwestRateLimiter {
|
|||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct TogglApi {
|
pub struct TogglApi {
|
||||||
client: ClientWithMiddleware,
|
client: ClientWithMiddleware,
|
||||||
api_key: String,
|
|
||||||
workspace_id: u32,
|
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 {
|
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 rate_limiter = ReqwestRateLimiter::new();
|
||||||
let backoff = ExponentialBackoff::builder()
|
let backoff = ExponentialBackoff::builder()
|
||||||
.retry_bounds(Duration::from_secs(1), Duration::from_secs(60))
|
.retry_bounds(Duration::from_secs(1), Duration::from_secs(60))
|
||||||
@ -49,6 +55,79 @@ impl TogglApi {
|
|||||||
.with(RetryTransientMiddleware::new_with_policy(backoff))
|
.with(RetryTransientMiddleware::new_with_policy(backoff))
|
||||||
.build();
|
.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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user