#![deny(warnings)]
#![deny(missing_docs)]
use anyhow::{bail, Error};
use chrono::{DateTime, Duration, NaiveTime, Utc};
use serde::{de::DeserializeOwned, de::Unexpected, Deserialize, Serialize};
use serde_json::{from_reader, from_str};
#[cfg(feature = "cache")]
pub mod cache;
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
#[serde(rename = "err")]
pub struct RTMError {
code: isize,
msg: String,
}
#[derive(Serialize, Deserialize, Default)]
pub struct RTMConfig {
pub api_key: Option<String>,
pub api_secret: Option<String>,
pub token: Option<String>,
pub user: Option<User>,
}
impl RTMConfig {
pub fn clear_user_data(&mut self) {
self.token = None;
self.user = None;
}
}
#[derive(Clone)]
pub struct API {
api_key: String,
api_secret: String,
token: Option<String>,
user: Option<User>,
#[cfg(test)]
server: std::rc::Rc<mockito::ServerGuard>,
}
#[derive(Deserialize, Debug, Serialize, Eq, PartialEq)]
struct FrobResponse {
stat: Stat,
frob: String,
}
#[derive(Deserialize, Serialize, Debug, Eq, PartialEq, Copy, Clone)]
#[serde(rename_all = "lowercase")]
pub enum Perms {
Read,
Write,
Delete,
}
impl Perms {
fn includes(self, other: Perms) -> bool {
matches!(
(self, other),
(Self::Delete, _)
| (Self::Write, Self::Read)
| (Self::Write, Self::Write)
| (Self::Read, Self::Read)
)
}
fn as_str(self) -> &'static str {
match self {
Self::Read => "read",
Self::Write => "write",
Self::Delete => "delete",
}
}
}
impl std::fmt::Display for Perms {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
impl std::str::FromStr for Perms {
type Err = String;
fn from_str(s: &str) -> Result<Self, String> {
match s {
"read" => Ok(Self::Read),
"write" => Ok(Self::Write),
"delete" => Ok(Self::Delete),
_ => Err("Invalid perms string".into()),
}
}
}
#[derive(Deserialize, Serialize, Debug, Eq, PartialEq, Clone)]
pub struct User {
id: String,
username: String,
fullname: String,
}
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
#[serde(rename = "auth")]
struct Auth {
token: String,
perms: Perms,
user: User,
}
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
#[serde(rename_all = "lowercase")]
enum Stat {
Ok,
Fail,
}
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
struct AuthResponse {
stat: Stat,
auth: Auth,
}
#[allow(dead_code)]
trait RTMToResult {
type Type;
fn into_result(self) -> Result<Self::Type, RTMError>;
}
impl RTMToResult for AuthResponse {
type Type = Auth;
fn into_result(self) -> Result<Auth, RTMError> {
match self.stat {
Stat::Ok => Ok(self.auth),
Stat::Fail => panic!(),
}
}
}
use serde::de::IntoDeserializer;
fn empty_string_as_none<'de, D, T>(de: D) -> Result<Option<T>, D::Error>
where
D: serde::Deserializer<'de>,
T: serde::Deserialize<'de>,
{
let opt = Option::<String>::deserialize(de)?;
let opt = opt.as_deref();
match opt {
None | Some("") => Ok(None),
Some(s) => T::deserialize(s.into_deserializer()).map(Some),
}
}
fn bool_from_string<'de, D>(deserializer: D) -> Result<bool, D::Error>
where
D: serde::Deserializer<'de>,
{
match String::deserialize(deserializer)?.as_ref() {
"0" => Ok(false),
"1" => Ok(true),
other => Err(serde::de::Error::invalid_value(
Unexpected::Str(other),
&"0 or 1",
)),
}
}
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
#[serde(untagged)]
enum TagSer {
List(Vec<()>),
Tags { tag: Vec<String> },
}
fn deser_tags<'de, D>(de: D) -> Result<Vec<String>, D::Error>
where
D: serde::Deserializer<'de>,
{
let res = TagSer::deserialize(de);
match res {
Err(e) => Err(e),
Ok(TagSer::List(_)) => Ok(vec![]),
Ok(TagSer::Tags { tag }) => Ok(tag),
}
}
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
#[serde(untagged)]
enum NoteSer {
List(Vec<()>),
Notes(RTMNotes),
}
fn deser_notes<'de, D>(de: D) -> Result<Vec<RTMNote>, D::Error>
where
D: serde::Deserializer<'de>,
{
let res = NoteSer::deserialize(de);
match res {
Err(e) => Err(e),
Ok(NoteSer::List(_)) => Ok(vec![]),
Ok(NoteSer::Notes(note)) => Ok(note.note),
}
}
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Clone)]
pub struct RRule {
#[serde(deserialize_with = "bool_from_string")]
pub every: bool,
#[serde(rename = "$t")]
pub rule: String,
}
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Clone)]
pub struct RTMNote {
pub id: String,
pub created: DateTime<Utc>,
pub modified: DateTime<Utc>,
pub title: String,
#[serde(rename = "$t")]
pub text: String,
}
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Clone)]
struct RTMNotes {
pub note: Vec<RTMNote>,
}
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Clone)]
pub struct TaskSeries {
pub id: String,
pub name: String,
pub created: DateTime<Utc>,
pub modified: DateTime<Utc>,
pub source: String,
pub url: String,
#[serde(default)]
#[serde(deserialize_with = "empty_string_as_none")]
pub parent_task_id: Option<String>,
#[serde(deserialize_with = "deser_notes")]
pub notes: Vec<RTMNote>,
pub task: Vec<Task>,
#[serde(deserialize_with = "deser_tags")]
pub tags: Vec<String>,
#[serde(rename = "rrule")]
pub repeat: Option<RRule>,
}
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Clone)]
pub struct Task {
pub id: String,
#[serde(deserialize_with = "empty_string_as_none")]
pub due: Option<DateTime<Utc>>,
#[serde(deserialize_with = "bool_from_string")]
pub has_due_time: bool,
#[serde(deserialize_with = "empty_string_as_none")]
pub deleted: Option<DateTime<Utc>>,
#[serde(deserialize_with = "empty_string_as_none")]
pub added: Option<DateTime<Utc>>,
#[serde(deserialize_with = "empty_string_as_none")]
pub completed: Option<DateTime<Utc>>,
pub priority: String,
}
#[derive(Debug, Copy, Clone)]
pub enum TimeLeft {
Remaining(u64),
Overdue(u64),
Completed,
NoDue,
}
impl Task {
pub fn get_time_left(&self) -> TimeLeft {
if self.completed.is_some() {
return TimeLeft::Completed;
}
if self.deleted.is_some() {
return TimeLeft::NoDue;
}
if self.due.is_none() || self.deleted.is_some() {
return TimeLeft::NoDue;
}
if let Some(mut due) = self.due {
if !self.has_due_time {
due += Duration::days(1);
}
let time_left = due.signed_duration_since(chrono::Utc::now());
let seconds = time_left.num_seconds();
if seconds < 0 {
TimeLeft::Overdue((-seconds) as u64)
} else {
TimeLeft::Remaining(seconds as u64)
}
} else {
unreachable!()
}
}
}
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Default)]
pub struct RTMTasks {
rev: String,
#[serde(default)]
pub list: Vec<RTMLists>,
}
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
pub struct RTMLists {
pub id: String,
pub taskseries: Option<Vec<TaskSeries>>,
}
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
struct TasksResponse {
stat: Stat,
tasks: RTMTasks,
}
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
#[serde(rename = "list")]
pub struct RTMList {
pub id: String,
pub name: String,
}
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
pub struct RTMTransaction {
pub id: String,
#[serde(deserialize_with = "bool_from_string")]
pub undoable: bool,
}
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
struct ListContainer {
list: Vec<RTMList>,
}
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
struct ListsResponse {
stat: Stat,
lists: ListContainer,
}
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
struct AddTagResponse {
stat: Stat,
list: RTMLists,
}
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
struct UndoResponse {
stat: Stat,
}
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
struct MarkDoneResponse {
stat: Stat,
transaction: Option<RTMTransaction>,
list: RTMLists,
}
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
struct SetURLResponse {
stat: Stat,
list: RTMLists,
}
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
struct AddTaskResponse {
stat: Stat,
transaction: Option<RTMTransaction>,
list: Option<RTMLists>,
}
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
struct SetDueDateResponse {
stat: Stat,
transaction: Option<RTMTransaction>,
list: Option<RTMLists>,
}
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
struct GetMethodsPayload {
method: Vec<String>,
}
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
struct GetMethodsResponse {
stat: Stat,
methods: GetMethodsPayload,
}
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
struct RTMResponse<T> {
rsp: T,
}
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
struct TimelineResponse {
stat: Stat,
timeline: String,
}
#[derive(Debug, Clone)]
pub struct RTMTimeline(String);
pub struct AuthState {
frob: String,
pub url: String,
}
impl API {
pub fn new(api_key: String, api_secret: String) -> API {
API {
api_key,
api_secret,
token: None,
user: None,
#[cfg(test)]
server: std::rc::Rc::new(mockito::Server::new()),
}
}
#[allow(missing_docs)]
#[cfg(test)]
pub fn new_test(api_key: String, api_secret: String, server: mockito::ServerGuard) -> API {
API {
api_key,
api_secret,
token: None,
user: None,
server: std::rc::Rc::new(server),
}
}
pub fn from_config(config: RTMConfig) -> API {
API {
api_key: config.api_key.unwrap(),
api_secret: config.api_secret.unwrap(),
token: config.token,
user: config.user,
#[cfg(test)]
server: std::rc::Rc::new(mockito::Server::new()),
}
}
#[allow(missing_docs)]
#[cfg(test)]
pub fn from_config_test(config: RTMConfig, server: mockito::ServerGuard) -> API {
API {
api_key: config.api_key.unwrap(),
api_secret: config.api_secret.unwrap(),
token: config.token,
user: config.user,
server: std::rc::Rc::new(server),
}
}
pub fn to_config(&self) -> RTMConfig {
RTMConfig {
api_key: Some(self.api_key.clone()),
api_secret: Some(self.api_secret.clone()),
token: self.token.clone(),
user: self.user.clone(),
}
}
fn sign_keys(&self, keys: &[(&str, &str)]) -> String {
let mut my_keys = keys.iter().collect::<Vec<&(&str, &str)>>();
my_keys.sort();
let mut to_sign = self.api_secret.clone();
for (k, v) in my_keys {
to_sign += k;
to_sign += v;
}
let digest = md5::compute(to_sign.as_bytes());
format!("{:x}", digest)
}
fn make_authenticated_url(&self, url: &str, keys: &[(&str, &str)]) -> String {
let mut url = url.to_string();
let auth_string = self.sign_keys(keys);
url.push('?');
for (k, v) in keys {
url += k;
url.push('=');
url += v;
url.push('&');
}
url += "api_sig=";
url += &auth_string;
url
}
async fn make_authenticated_request<'a>(
&'a self,
url: &'a str,
keys: &'a [(&'a str, &'a str)],
) -> Result<String, anyhow::Error> {
let auth_string = self.sign_keys(keys);
let client = reqwest::Client::new();
log::trace!("make_authenticated_request: keys={:?}", keys);
let req = client
.request(reqwest::Method::GET, url)
.query(keys)
.query(&[("api_sig", auth_string)])
.build()?;
log::trace!("make_authenticated_request: url={}", req.url());
let body = client.execute(req).await?.text().await?;
log::trace!("make_authenticated_request: reply body={}", body);
Ok(body)
}
async fn get_frob(&self) -> Result<String, Error> {
let response = self
.make_authenticated_request(
&self.get_rest_url(),
&[
("method", "rtm.auth.getFrob"),
("format", "json"),
("api_key", &self.api_key),
],
)
.await?;
let frob_resp = from_str::<RTMResponse<FrobResponse>>(&response)
.unwrap()
.rsp;
Ok(frob_resp.frob)
}
#[cfg(test)]
fn get_auth_url(&self) -> String {
self.server.url()
}
#[cfg(not(test))]
fn get_auth_url(&self) -> String {
static MILK_AUTH_URL: &str = "https://www.rememberthemilk.com/services/auth/";
MILK_AUTH_URL.to_string()
}
#[cfg(test)]
fn get_rest_url(&self) -> String {
self.server.url()
}
#[cfg(not(test))]
fn get_rest_url(&self) -> String {
static MILK_REST_URL: &str = "https://api.rememberthemilk.com/services/rest/";
MILK_REST_URL.to_string()
}
pub async fn start_auth(&self, perm: Perms) -> Result<AuthState, Error> {
let frob = self.get_frob().await?;
let url = self.make_authenticated_url(
&self.get_auth_url(),
&[
("api_key", &self.api_key),
("format", "json"),
("perms", perm.as_str()),
("frob", &frob),
],
);
Ok(AuthState { frob, url })
}
pub async fn check_auth(&mut self, auth: &AuthState) -> Result<bool, Error> {
let response = self
.make_authenticated_request(
&self.get_rest_url(),
&[
("method", "rtm.auth.getToken"),
("format", "json"),
("api_key", &self.api_key),
("frob", &auth.frob),
],
)
.await?;
let auth_rep = from_str::<RTMResponse<AuthResponse>>(&response)
.unwrap()
.rsp;
self.token = Some(auth_rep.auth.token);
self.user = Some(auth_rep.auth.user);
Ok(true)
}
pub async fn has_token(&self, perm: Perms) -> Result<bool, Error> {
if let Some(ref tok) = self.token {
let response = self
.make_authenticated_request(
&self.get_rest_url(),
&[
("method", "rtm.auth.checkToken"),
("format", "json"),
("api_key", &self.api_key),
("auth_token", tok),
],
)
.await?;
let ar = from_str::<RTMResponse<AuthResponse>>(&response)?.rsp;
Ok(ar.auth.perms.includes(perm))
} else {
Ok(false)
}
}
pub async fn get_all_tasks(&self) -> Result<RTMTasks, Error> {
self.get_tasks_filtered("").await
}
async fn get_tasks_filtered_sync_typed<T>(
&self,
filter: &str,
last_sync: Option<chrono::DateTime<Utc>>,
) -> Result<T, Error>
where
T: DeserializeOwned,
{
if let Some(ref tok) = self.token {
let mut params = vec![
("method", "rtm.tasks.getList"),
("format", "json"),
("api_key", &self.api_key),
("auth_token", tok),
("v", "2"),
];
if !filter.is_empty() {
params.push(("filter", filter));
}
let ls_str;
if let Some(ls) = last_sync {
ls_str = ls.to_rfc3339();
params.push(("last_sync", &ls_str));
}
let response = self
.make_authenticated_request(&self.get_rest_url(), ¶ms)
.await?;
Ok(from_reader::<_, T>(response.as_bytes())?)
} else {
bail!("Unable to fetch tasks")
}
}
pub async fn get_tasks_filtered_sync(
&self,
filter: &str,
last_sync: Option<chrono::DateTime<Utc>>,
) -> Result<RTMTasks, Error> {
Ok(self
.get_tasks_filtered_sync_typed::<RTMResponse<TasksResponse>>(filter, last_sync)
.await?
.rsp
.tasks)
}
pub async fn get_tasks_filtered_sync_json(
&self,
filter: &str,
last_sync: Option<chrono::DateTime<Utc>>,
) -> Result<serde_json::Value, Error> {
Ok(self
.get_tasks_filtered_sync_typed::<serde_json::Value>(filter, last_sync)
.await?
.get_mut("rsp")
.ok_or_else(|| anyhow::anyhow!("Response did not have rsp field"))?
.get_mut("tasks")
.ok_or_else(|| anyhow::anyhow!("Response did not have task field"))?
.take())
}
pub async fn get_tasks_filtered(&self, filter: &str) -> Result<RTMTasks, Error> {
self.get_tasks_filtered_sync(filter, None).await
}
pub fn get_filter_extid(&self, extid: &str) -> String {
let filter = format!("source:api:{}:{}", self.api_key, extid);
filter
}
pub async fn get_tasks_in_list(&self, list_id: &str, filter: &str) -> Result<RTMTasks, Error> {
if let Some(ref tok) = self.token {
let mut params = vec![
("method", "rtm.tasks.getList"),
("format", "json"),
("api_key", &self.api_key),
("auth_token", tok),
("v", "2"),
("list_id", list_id),
];
if !filter.is_empty() {
params.push(("filter", filter));
}
let response = self
.make_authenticated_request(&self.get_rest_url(), ¶ms)
.await?;
let tasklist = from_str::<RTMResponse<TasksResponse>>(&response)
.unwrap()
.rsp
.tasks;
Ok(tasklist)
} else {
bail!("Unable to fetch tasks")
}
}
pub async fn get_lists(&self) -> Result<Vec<RTMList>, Error> {
if let Some(ref tok) = self.token {
let params = &[
("method", "rtm.lists.getList"),
("format", "json"),
("api_key", &self.api_key),
("auth_token", tok),
];
let response = self
.make_authenticated_request(&self.get_rest_url(), params)
.await?;
let lists = from_str::<RTMResponse<ListsResponse>>(&response)
.unwrap()
.rsp
.lists;
Ok(lists.list)
} else {
bail!("Unable to fetch tasks")
}
}
pub async fn get_timeline(&self) -> Result<RTMTimeline, Error> {
if let Some(ref tok) = self.token {
let params = &[
("method", "rtm.timelines.create"),
("format", "json"),
("api_key", &self.api_key),
("auth_token", tok),
];
let response = self
.make_authenticated_request(&self.get_rest_url(), params)
.await?;
let tl = from_str::<RTMResponse<TimelineResponse>>(&response)
.unwrap()
.rsp
.timeline;
Ok(RTMTimeline(tl))
} else {
bail!("Unable to fetch tasks")
}
}
pub async fn undo_transaction(
&self,
timeline: &RTMTimeline,
transaction_id: &str,
) -> Result<(), Error> {
if let Some(ref tok) = self.token {
let params = &[
("method", "rtm.transactions.undo"),
("format", "json"),
("api_key", &self.api_key),
("auth_token", tok),
("timeline", &timeline.0),
("transaction_id", transaction_id),
];
let response = self
.make_authenticated_request(&self.get_rest_url(), params)
.await?;
let rsp = from_str::<RTMResponse<UndoResponse>>(&response)?.rsp;
if let Stat::Ok = rsp.stat {
Ok(())
} else {
bail!("Error undoing: {:?}", rsp.stat)
}
} else {
bail!("Unable to undo")
}
}
pub async fn set_url(
&self,
timeline: &RTMTimeline,
list: &RTMLists,
taskseries: &TaskSeries,
task: &Task,
url: &str,
) -> Result<(), Error> {
if let Some(ref tok) = self.token {
let params = &[
("method", "rtm.tasks.setURL"),
("format", "json"),
("api_key", &self.api_key),
("auth_token", tok),
("timeline", &timeline.0),
("list_id", &list.id),
("taskseries_id", &taskseries.id),
("task_id", &task.id),
("url", url),
];
let response = self
.make_authenticated_request(&self.get_rest_url(), params)
.await?;
let rsp = from_str::<RTMResponse<SetURLResponse>>(&response)?.rsp;
if let Stat::Ok = rsp.stat {
Ok(())
} else {
bail!("Error adding task")
}
} else {
bail!("Unable to fetch tasks")
}
}
pub async fn add_tag(
&self,
timeline: &RTMTimeline,
list: &RTMLists,
taskseries: &TaskSeries,
task: &Task,
tags: &[&str],
) -> Result<(), Error> {
if let Some(ref tok) = self.token {
let tags = tags.join(",");
let params = &[
("method", "rtm.tasks.addTags"),
("format", "json"),
("api_key", &self.api_key),
("auth_token", tok),
("timeline", &timeline.0),
("list_id", &list.id),
("taskseries_id", &taskseries.id),
("task_id", &task.id),
("tags", &tags),
];
let response = self
.make_authenticated_request(&self.get_rest_url(), params)
.await?;
let rsp = from_str::<RTMResponse<AddTagResponse>>(&response)?.rsp;
if let Stat::Ok = rsp.stat {
Ok(())
} else {
bail!("Error adding task")
}
} else {
bail!("Unable to add task")
}
}
pub async fn mark_complete(
&self,
timeline: &RTMTimeline,
list: &RTMLists,
taskseries: &TaskSeries,
task: &Task,
) -> Result<Option<RTMTransaction>, Error> {
self.mark_complete_id(timeline, &list.id, &taskseries.id, &task.id)
.await
}
pub async fn mark_complete_id(
&self,
timeline: &RTMTimeline,
list_id: &str,
taskseries_id: &str,
task_id: &str,
) -> Result<Option<RTMTransaction>, Error> {
if let Some(ref tok) = self.token {
let params = &[
("method", "rtm.tasks.complete"),
("format", "json"),
("api_key", &self.api_key),
("auth_token", tok),
("timeline", &timeline.0),
("list_id", list_id),
("taskseries_id", taskseries_id),
("task_id", task_id),
];
let response = self
.make_authenticated_request(&self.get_rest_url(), params)
.await?;
let rsp = from_str::<RTMResponse<MarkDoneResponse>>(&response)?.rsp;
if let Stat::Ok = rsp.stat {
Ok(rsp.transaction)
} else {
bail!("Error completing task")
}
} else {
bail!("Unable to complete task")
}
}
pub async fn add_task(
&self,
timeline: &RTMTimeline,
name: &str,
list: Option<&RTMLists>,
parent: Option<&Task>,
external_id: Option<&str>,
smart: bool,
) -> Result<Option<RTMLists>, Error> {
if let Some(ref tok) = self.token {
let mut params = vec![
("method", "rtm.tasks.add"),
("format", "json"),
("api_key", &self.api_key),
("auth_token", tok),
("timeline", &timeline.0),
("name", name),
];
if let Some(list) = list {
params.push(("list_id", &list.id));
}
if let Some(parent) = parent {
params.push(("task_id", &parent.id));
}
if let Some(external_id) = external_id {
params.push(("external_id", external_id));
}
if smart {
params.push(("parse", "1"));
}
let response = self
.make_authenticated_request(&self.get_rest_url(), ¶ms)
.await?;
log::trace!("Add task response: {}", response);
let rsp = from_str::<RTMResponse<AddTaskResponse>>(&response)?.rsp;
if let Stat::Ok = rsp.stat {
if let Some(list) = rsp.list {
if let Some(series) = &list.taskseries {
if !series.is_empty() {
Ok(Some(list))
} else {
Ok(None)
}
} else {
Ok(None)
}
} else {
Ok(None)
}
} else {
bail!("Error adding task")
}
} else {
bail!("Unable to fetch tasks")
}
}
pub async fn set_due_date(
&self,
timeline: &RTMTimeline,
list: &RTMLists,
taskseries: &TaskSeries,
task: &Task,
due: DateTime<Utc>,
) -> Result<Option<TaskSeries>, Error> {
if let Some(ref tok) = self.token {
let mut params = vec![
("method", "rtm.tasks.setDueDate"),
("format", "json"),
("api_key", &self.api_key),
("auth_token", tok),
("timeline", &timeline.0),
("list_id", &list.id),
("taskseries_id", &taskseries.id),
("task_id", &task.id),
];
let date_str = due.to_rfc3339();
params.push(("due", &date_str));
if due.time() != NaiveTime::from_hms_opt(0, 0, 0).unwrap() {
params.push(("has_due_time", "1"));
}
let response = self
.make_authenticated_request(&self.get_rest_url(), ¶ms)
.await?;
log::trace!("Set due date response: {}", response);
let rsp = from_str::<RTMResponse<SetDueDateResponse>>(&response)?.rsp;
if let Stat::Ok = rsp.stat {
if let Some(list) = rsp.list {
if let Some(mut series) = list.taskseries {
if !series.is_empty() {
Ok(Some(series.pop().unwrap()))
} else {
Ok(None)
}
} else {
Ok(None)
}
} else {
Ok(None)
}
} else {
bail!("Error adding task")
}
} else {
bail!("Unable to fetch tasks")
}
}
pub async fn get_methods(&self) -> Result<Vec<String>, Error> {
let params = vec![
("method", "rtm.reflection.getMethods"),
("format", "json"),
("api_key", &self.api_key),
];
let response = self
.make_authenticated_request(&self.get_rest_url(), ¶ms)
.await?;
log::trace!("Set due date response: {}", response);
let rsp = from_str::<RTMResponse<GetMethodsResponse>>(&response)?.rsp;
Ok(rsp.methods.method)
}
}
#[cfg(test)]
mod tests;