Centralise files related to talking to the Toggl apis to the toggl_api module
This commit is contained in:
@@ -0,0 +1,178 @@
|
||||
use reqwest::Client;
|
||||
use serde_json::Value;
|
||||
use std::collections::HashMap;
|
||||
use std::time::Duration;
|
||||
use hyper::HeaderMap;
|
||||
use tracing::instrument;
|
||||
use tracing::log::debug;
|
||||
use crate::toggl_api::types::{Current, Project, ProjectClient, ReportEntry, TogglQuery};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct TogglApiClient {
|
||||
client: Client,
|
||||
workspace_id: String,
|
||||
base_url: String,
|
||||
reports_base_url: String,
|
||||
|
||||
headers: HeaderMap,
|
||||
}
|
||||
|
||||
impl TogglApiClient {
|
||||
pub async fn check_health(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
pub fn new(workspace_id: &str, toggl_auth: &str) -> Self {
|
||||
let client = Client::builder()
|
||||
.default_headers(Self::default_headers(toggl_auth))
|
||||
.build()
|
||||
.expect("Failed to build reqwest client");
|
||||
|
||||
Self {
|
||||
client,
|
||||
workspace_id: workspace_id.to_string(),
|
||||
base_url: "https://api.track.toggl.com/api/v9".to_string(),
|
||||
reports_base_url: "https://api.track.toggl.com/reports/api/v3".to_string(),
|
||||
headers: Self::default_headers(toggl_auth),
|
||||
}
|
||||
}
|
||||
|
||||
fn default_headers(toggl_auth: &str) -> reqwest::header::HeaderMap {
|
||||
let mut headers = reqwest::header::HeaderMap::new();
|
||||
headers.insert(
|
||||
"Authorization",
|
||||
reqwest::header::HeaderValue::from_str(&format!("Basic {}", toggl_auth)).unwrap(),
|
||||
);
|
||||
headers
|
||||
}
|
||||
|
||||
pub async fn fetch_projects(&self) -> Result<Vec<Project>, reqwest::Error> {
|
||||
let url = format!(
|
||||
"{base_url}/workspaces/{}/projects",
|
||||
self.workspace_id,
|
||||
base_url = self.base_url,
|
||||
);
|
||||
|
||||
let res = self
|
||||
.client
|
||||
.get(&url)
|
||||
.headers(self.headers.clone())
|
||||
.send()
|
||||
.await?
|
||||
.json::<Vec<Project>>()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
pub async fn fetch_clients(&self) -> Result<Vec<ProjectClient>, reqwest::Error> {
|
||||
let url = format!(
|
||||
"{base_url}/workspaces/{}/clients",
|
||||
self.workspace_id,
|
||||
base_url = self.base_url,
|
||||
);
|
||||
|
||||
let res = self
|
||||
.client
|
||||
.get(&url)
|
||||
.headers(self.headers.clone())
|
||||
.send()
|
||||
.await?
|
||||
.json::<Vec<ProjectClient>>()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
pub async fn get_current(&self) -> Result<Option<Current>, reqwest::Error> {
|
||||
let url = format!(
|
||||
"{base_url}/me/time_entries/current",
|
||||
base_url = self.base_url
|
||||
);
|
||||
|
||||
let res = self
|
||||
.client
|
||||
.get(url)
|
||||
.send()
|
||||
.await?
|
||||
.json::<Option<Current>>()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
fn create_filters(original_filters: &TogglQuery, last_row_id: u64) -> TogglQuery {
|
||||
let mut filters: TogglQuery = original_filters.clone();
|
||||
filters.first_row_number = Some(last_row_id + 1);
|
||||
filters
|
||||
}
|
||||
|
||||
#[instrument(skip(self, filters))]
|
||||
pub async fn full_report(
|
||||
&self,
|
||||
filters: &TogglQuery,
|
||||
) -> anyhow::Result<Vec<ReportEntry>> {
|
||||
let url = format!(
|
||||
"{base_url}/workspace/{workspace_id}/search/time_entries",
|
||||
base_url = self.reports_base_url,
|
||||
workspace_id = self.workspace_id
|
||||
);
|
||||
|
||||
let mut last_row_number = Some(0);
|
||||
let mut results = vec![];
|
||||
|
||||
while let Some(last_row_number_n) = last_row_number {
|
||||
debug!("Fetching page starting with {}", last_row_number_n);
|
||||
// If we are not on the first page, wait a bit to avoid rate limiting
|
||||
if last_row_number_n != 0 {
|
||||
tokio::time::sleep(Duration::from_millis(1000)).await;
|
||||
}
|
||||
|
||||
// TODO: Implement rate limiting
|
||||
let response = self
|
||||
.client
|
||||
.post(&url)
|
||||
.headers(self.headers.clone())
|
||||
.json(&Self::create_filters(&filters, last_row_number_n))
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
let data = response
|
||||
.json::<Vec<ReportEntry>>()
|
||||
.await?;
|
||||
|
||||
last_row_number = data.last().map(|e| e.row_number as u64);
|
||||
|
||||
data.into_iter().for_each(|e| results.push(e));
|
||||
}
|
||||
|
||||
Ok(results)
|
||||
}
|
||||
|
||||
pub async fn start_time_entry(&self, mut body: HashMap<String, Value>) -> anyhow::Result<()> {
|
||||
let url = format!(
|
||||
"{base_url}/workspaces/{workspace_id}/time_entries",
|
||||
base_url = self.base_url,
|
||||
workspace_id = self.workspace_id
|
||||
);
|
||||
|
||||
body.insert(
|
||||
"workspace_id".to_string(),
|
||||
self.workspace_id.parse::<i32>().unwrap().into(),
|
||||
);
|
||||
|
||||
dbg!(self.client
|
||||
.post(url)
|
||||
.headers(self.headers.clone())
|
||||
.json(&body)
|
||||
.send()
|
||||
.await?
|
||||
.text()
|
||||
.await?);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
pub mod api_client;
|
||||
pub mod types;
|
||||
|
||||
pub use api_client::TogglApiClient;
|
||||
@@ -0,0 +1,200 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
use serde_with::skip_serializing_none;
|
||||
use std::collections::HashMap;
|
||||
use std::option::Option;
|
||||
use chrono::{DateTime, Utc};
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, Debug)]
|
||||
pub struct TimeEntry {
|
||||
pub id: u64,
|
||||
pub seconds: u32,
|
||||
pub start: String,
|
||||
pub stop: String,
|
||||
pub at: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, Debug)]
|
||||
pub struct ReportEntry {
|
||||
pub user_id: u32,
|
||||
pub username: String,
|
||||
pub project_id: Option<u64>,
|
||||
pub task_id: Option<u64>,
|
||||
pub billable: bool,
|
||||
pub description: String,
|
||||
pub tag_ids: Vec<u64>,
|
||||
pub billable_amount_in_cents: Option<u64>,
|
||||
pub hourly_rate_in_cents: Option<u64>,
|
||||
pub currency: String,
|
||||
pub time_entries: Vec<TimeEntry>,
|
||||
pub row_number: u32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct Current {
|
||||
pub id: u64,
|
||||
pub workspace_id: u64,
|
||||
pub project_id: Option<u64>,
|
||||
pub task_id: Option<u64>,
|
||||
pub billable: bool,
|
||||
pub start: String,
|
||||
pub stop: Option<String>,
|
||||
pub duration: i64,
|
||||
pub description: String,
|
||||
pub tags: Vec<String>,
|
||||
pub tag_ids: Vec<u64>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct Project {
|
||||
pub id: u64,
|
||||
pub workspace_id: u64,
|
||||
pub client_id: Option<u64>,
|
||||
pub name: String,
|
||||
pub active: bool,
|
||||
|
||||
#[serde(flatten)]
|
||||
pub rest: HashMap<String, Value>,
|
||||
}
|
||||
|
||||
/// Represents a client in Toggl.
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct ProjectClient {
|
||||
/// Indicates whether the client is archived or not.
|
||||
pub archived: bool,
|
||||
|
||||
/// Represents the timestamp of the last update made to the client.
|
||||
pub at: DateTime<Utc>,
|
||||
|
||||
/// The unique identifier for the client.
|
||||
pub id: i32,
|
||||
|
||||
/// The name of the client.
|
||||
pub name: String,
|
||||
|
||||
/// Indicates the timestamp when the client was deleted. If the client is not deleted, this property will be null.
|
||||
pub server_deleted_at: Option<DateTime<Utc>>,
|
||||
|
||||
/// The Workspace ID associated with the client.
|
||||
pub wid: i32,
|
||||
}
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
#[skip_serializing_none]
|
||||
#[derive(Serialize, Deserialize, Clone, Default)]
|
||||
pub struct TogglQuery {
|
||||
pub billable: Option<bool>,
|
||||
pub client_ids: Option<Vec<u64>>,
|
||||
pub description: Option<String>,
|
||||
pub end_date: Option<String>,
|
||||
pub first_id: Option<u64>,
|
||||
pub first_row_number: Option<u64>,
|
||||
pub first_timestamp: Option<u64>,
|
||||
pub group_ids: Option<Vec<u64>>,
|
||||
pub grouped: Option<bool>,
|
||||
pub hide_amounts: Option<bool>,
|
||||
pub max_duration_seconds: Option<u64>,
|
||||
pub min_duration_seconds: Option<u64>,
|
||||
pub order_by: Option<String>,
|
||||
pub order_dir: Option<String>,
|
||||
pub postedFields: Option<Vec<String>>,
|
||||
pub project_ids: Option<Vec<u64>>,
|
||||
pub rounding: Option<u64>,
|
||||
pub rounding_minutes: Option<u64>,
|
||||
pub startTime: Option<String>,
|
||||
pub start_date: Option<String>,
|
||||
pub tag_ids: Option<Vec<u64>>,
|
||||
pub task_ids: Option<Vec<u64>>,
|
||||
pub time_entry_ids: Option<Vec<u64>>,
|
||||
pub user_ids: Option<Vec<u64>>,
|
||||
|
||||
#[serde(flatten)]
|
||||
pub rest: HashMap<String, Value>,
|
||||
}
|
||||
|
||||
use std::fmt;
|
||||
|
||||
impl fmt::Debug for TogglQuery {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let mut ds = f.debug_struct("TogglQuery");
|
||||
|
||||
if let Some(billable) = &self.billable {
|
||||
ds.field("billable", billable);
|
||||
}
|
||||
if let Some(client_ids) = &self.client_ids {
|
||||
ds.field("client_ids", client_ids);
|
||||
}
|
||||
if let Some(description) = &self.description {
|
||||
ds.field("description", description);
|
||||
}
|
||||
if let Some(end_date) = &self.end_date {
|
||||
ds.field("end_date", end_date);
|
||||
}
|
||||
if let Some(first_id) = &self.first_id {
|
||||
ds.field("first_id", first_id);
|
||||
}
|
||||
if let Some(first_row_number) = &self.first_row_number {
|
||||
ds.field("first_row_number", first_row_number);
|
||||
}
|
||||
if let Some(first_timestamp) = &self.first_timestamp {
|
||||
ds.field("first_timestamp", first_timestamp);
|
||||
}
|
||||
if let Some(group_ids) = &self.group_ids {
|
||||
ds.field("group_ids", group_ids);
|
||||
}
|
||||
if let Some(grouped) = &self.grouped {
|
||||
ds.field("grouped", grouped);
|
||||
}
|
||||
if let Some(hide_amounts) = &self.hide_amounts {
|
||||
ds.field("hide_amounts", hide_amounts);
|
||||
}
|
||||
if let Some(max_duration_seconds) = &self.max_duration_seconds {
|
||||
ds.field("max_duration_seconds", max_duration_seconds);
|
||||
}
|
||||
if let Some(min_duration_seconds) = &self.min_duration_seconds {
|
||||
ds.field("min_duration_seconds", min_duration_seconds);
|
||||
}
|
||||
if let Some(order_by) = &self.order_by {
|
||||
ds.field("order_by", order_by);
|
||||
}
|
||||
if let Some(order_dir) = &self.order_dir {
|
||||
ds.field("order_dir", order_dir);
|
||||
}
|
||||
if let Some(postedFields) = &self.postedFields {
|
||||
ds.field("postedFields", postedFields);
|
||||
}
|
||||
if let Some(project_ids) = &self.project_ids {
|
||||
ds.field("project_ids", project_ids);
|
||||
}
|
||||
if let Some(rounding) = &self.rounding {
|
||||
ds.field("rounding", rounding);
|
||||
}
|
||||
if let Some(rounding_minutes) = &self.rounding_minutes {
|
||||
ds.field("rounding_minutes", rounding_minutes);
|
||||
}
|
||||
if let Some(startTime) = &self.startTime {
|
||||
ds.field("startTime", startTime);
|
||||
}
|
||||
if let Some(start_date) = &self.start_date {
|
||||
ds.field("start_date", start_date);
|
||||
}
|
||||
if let Some(tag_ids) = &self.tag_ids {
|
||||
ds.field("tag_ids", tag_ids);
|
||||
}
|
||||
if let Some(task_ids) = &self.task_ids {
|
||||
ds.field("task_ids", task_ids);
|
||||
}
|
||||
if let Some(time_entry_ids) = &self.time_entry_ids {
|
||||
ds.field("time_entry_ids", time_entry_ids);
|
||||
}
|
||||
if let Some(user_ids) = &self.user_ids {
|
||||
ds.field("user_ids", user_ids);
|
||||
}
|
||||
|
||||
if !self.rest.is_empty() {
|
||||
ds.field("rest", &self.rest);
|
||||
}
|
||||
|
||||
ds.finish()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user