use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct TaskList {
pub id: String,
pub title: String,
pub updated: Option<String>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct TaskListsResponse {
pub items: Option<Vec<TaskList>>,
#[serde(rename = "nextPageToken")]
pub next_page_token: Option<String>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct TaskGet {
pub id: String,
pub title: Option<String>,
pub status: Option<String>, pub notes: Option<String>, pub due: Option<String>, pub completed: Option<String>, pub parent: Option<String>, pub updated: Option<String>, pub deleted: Option<bool>, }
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct TaskLocal {
pub id: String,
pub list_id: String,
pub title: Option<String>,
pub is_completed: bool, pub notes: Option<String>, pub due: Option<chrono::DateTime<chrono::Utc>>, pub completed: Option<chrono::DateTime<chrono::Utc>>, pub parent: Option<String>, pub updated: Option<chrono::DateTime<chrono::Utc>>, pub is_dirty: bool,
pub is_deleted: bool,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct TasksResponse {
pub items: Option<Vec<TaskGet>>,
#[serde(rename = "nextPageToken")]
pub next_page_token: Option<String>,
}
#[derive(Debug, Clone)]
pub struct GoogleTasksClient {
pub client: reqwest::Client,
pub access_token: String,
}
impl GoogleTasksClient {
pub fn new(access_token: String) -> Self {
Self {
client: reqwest::Client::new(),
access_token,
}
}
pub async fn get_task_lists(&mut self) -> Result<Vec<TaskList>, reqwest::Error> {
let url = "https://www.googleapis.com/tasks/v1/users/@me/lists";
let mut all_lists = Vec::new();
let mut page_token: Option<String> = None;
loop {
let response = self
.execute_with_retry(|client, token| {
let mut req = client.get(url).bearer_auth(token);
if let Some(ref pt) = page_token {
req = req.query(&[("pageToken", pt)]);
}
req
})
.await?;
let response = response.error_for_status()?;
let task_lists_response: TaskListsResponse = response.json().await?;
if let Some(items) = task_lists_response.items {
all_lists.extend(items);
}
match task_lists_response.next_page_token {
Some(token) if !token.is_empty() => page_token = Some(token),
_ => break,
}
}
Ok(all_lists)
}
pub async fn create_task_list(&mut self, title: &str) -> Result<TaskList, reqwest::Error> {
let url = "https://www.googleapis.com/tasks/v1/users/@me/lists";
let payload = serde_json::json!({ "title": title });
let response = self
.execute_with_retry(|client, token| client.post(url).bearer_auth(token).json(&payload))
.await?;
let response = response.error_for_status()?;
let created: TaskList = response.json().await?;
Ok(created)
}
pub async fn delete_task_list(&mut self, list_id: &str) -> Result<(), reqwest::Error> {
let url = format!(
"https://www.googleapis.com/tasks/v1/users/@me/lists/{}",
list_id
);
let response = self
.execute_with_retry(|client, token| client.delete(&url).bearer_auth(token))
.await?;
if response.status() == reqwest::StatusCode::NOT_FOUND
|| response.status() == reqwest::StatusCode::NO_CONTENT
{
return Ok(());
}
response.error_for_status()?;
Ok(())
}
pub async fn get_tasks(
&mut self,
list_id: &str,
show_completed: bool,
updated_min: Option<&chrono::DateTime<chrono::Utc>>,
) -> Result<Vec<TaskGet>, reqwest::Error> {
let url = format!(
"https://www.googleapis.com/tasks/v1/lists/{}/tasks",
list_id
);
let updated_min_str = updated_min.map(|dt| dt.to_rfc3339());
let mut all_tasks = Vec::new();
let mut page_token: Option<String> = None;
loop {
let response = self
.execute_with_retry(|client, token| {
let mut query = vec![
("showCompleted", show_completed.to_string()),
("showHidden", show_completed.to_string()),
("showDeleted", "true".to_string()),
];
if let Some(ref dt_str) = updated_min_str {
query.push(("updatedMin", dt_str.clone()));
}
if let Some(ref pt) = page_token {
query.push(("pageToken", pt.clone()));
}
client.get(&url).bearer_auth(token).query(&query)
})
.await?;
let response = response.error_for_status()?;
let tasks_response: TasksResponse = response.json().await?;
if let Some(items) = tasks_response.items {
all_tasks.extend(items);
}
match tasks_response.next_page_token {
Some(token) if !token.is_empty() => page_token = Some(token),
_ => break,
}
}
Ok(all_tasks)
}
pub async fn create_task(
&mut self,
list_id: &str,
task: &TaskLocal,
) -> Result<TaskGet, reqwest::Error> {
let url = format!(
"https://www.googleapis.com/tasks/v1/lists/{}/tasks",
list_id
);
let body = serde_json::json!({
"title": task.title,
"notes": task.notes,
"due": task.due.map(|d| d.to_rfc3339()),
"parent": task.parent,
});
let response = self
.execute_with_retry(|client, token| client.post(&url).bearer_auth(token).json(&body))
.await?;
let response = response.error_for_status()?;
let created_task: TaskGet = response.json().await?;
Ok(created_task)
}
pub async fn update_task(
&mut self,
list_id: &str,
task_id: &str,
title: Option<&str>,
notes: Option<&str>,
completed: Option<bool>,
due: Option<&chrono::DateTime<chrono::Utc>>,
) -> Result<TaskGet, reqwest::Error> {
let url = format!(
"https://www.googleapis.com/tasks/v1/lists/{list_id}/tasks/{task_id}",
list_id = list_id,
task_id = task_id
);
let mut body = serde_json::Map::new();
if let Some(title) = title {
body.insert(
"title".to_string(),
serde_json::Value::String(title.to_string()),
);
}
if let Some(notes) = notes {
body.insert(
"notes".to_string(),
serde_json::Value::String(notes.to_string()),
);
}
if let Some(due) = due {
body.insert(
"due".to_string(),
serde_json::Value::String(due.to_rfc3339()),
);
}
if let Some(completed) = completed {
let status_str = if completed {
"completed"
} else {
"needsAction"
};
body.insert(
"status".to_string(),
serde_json::Value::String(status_str.to_string()),
);
}
let response = self
.execute_with_retry(|client, token| client.patch(&url).bearer_auth(token).json(&body))
.await?;
let response = response.error_for_status()?;
let toggled_task: TaskGet = response.json().await?;
Ok(toggled_task)
}
pub async fn toggle_task_completion(
&mut self,
list_id: &str,
task_id: &str,
completed: bool,
) -> Result<TaskGet, reqwest::Error> {
self.update_task(list_id, task_id, None, None, Some(completed), None)
.await
}
async fn execute_with_retry(
&mut self,
build_request: impl Fn(&reqwest::Client, &str) -> reqwest::RequestBuilder,
) -> Result<reqwest::Response, reqwest::Error> {
let request = build_request(&self.client, &self.access_token);
let response = request.send().await?;
if response.status() == reqwest::StatusCode::UNAUTHORIZED {
if let Ok(refresh_token) = crate::auth::keyring::get_refresh_token() {
if let Ok(token_response) = crate::auth::refresh_access_token(&refresh_token).await
{
self.access_token = token_response.access_token;
let retry_token = build_request(&self.client, &self.access_token);
return retry_token.send().await;
}
}
}
Ok(response)
}
pub async fn delete_task(
&mut self,
list_id: &str,
task_id: &str,
) -> Result<(), reqwest::Error> {
let url = format!(
"https://www.googleapis.com/tasks/v1/lists/{list_id}/tasks/{task_id}",
list_id = list_id,
task_id = task_id
);
let response = self
.execute_with_retry(|client, token| client.delete(&url).bearer_auth(token))
.await?;
if response.status() == reqwest::StatusCode::NOT_FOUND {
return Ok(());
}
response.error_for_status()?;
Ok(())
}
}
impl TaskLocal {
pub fn from_task_get(task_get: TaskGet, list_id: String) -> Self {
let due = task_get.due.as_ref().and_then(|due_str| {
DateTime::parse_from_rfc3339(due_str)
.ok()
.map(|dt| dt.with_timezone(&Utc))
});
let completed = task_get.completed.as_ref().and_then(|completed_str| {
DateTime::parse_from_rfc3339(completed_str)
.ok()
.map(|dt| dt.with_timezone(&Utc))
});
let updated = task_get.updated.as_ref().and_then(|updated_str| {
DateTime::parse_from_rfc3339(updated_str)
.ok()
.map(|dt| dt.with_timezone(&Utc))
});
let is_dirty = false;
let is_deleted = task_get.deleted.unwrap_or(false);
TaskLocal {
id: task_get.id,
list_id,
title: task_get.title,
is_completed: task_get.status.map(|s| s == "completed").unwrap_or(false),
notes: task_get.notes,
due,
completed,
parent: task_get.parent,
updated,
is_dirty,
is_deleted,
}
}
pub fn toggle_task_completion(&self) -> TaskLocal {
let mut updated_task = self.clone();
updated_task.is_completed = !self.is_completed;
if updated_task.is_completed {
updated_task.completed = Some(Utc::now());
} else {
updated_task.completed = None;
}
updated_task
}
}