|
class Todoist { |
|
static API_TOKEN() { |
|
return '<YOUR API TOKEN HERE>'; |
|
} |
|
|
|
kebab(string) { |
|
return string |
|
.replace(/([a-z])([A-Z]+)/g, (_, s1, s2) => `${s1} ${s2}`) |
|
.replace( |
|
/([A-Z])([A-Z]+)([^a-zA-Z0-9]*)$/, |
|
(_, s1, s2, s3) => `${s1} ${s2.toLowerCase()} ${s3}`, |
|
) |
|
.replace( |
|
/([A-Z]+)([A-Z][a-z])/g, |
|
(_, s1, s2) => `${s1.toLowerCase()} ${s2}`, |
|
) |
|
.replace(/\W+/g, ' ') |
|
.replace(/_/g, '-') |
|
.split(/ |\B(?=[A-Z])/) |
|
.map((word) => word.toLowerCase()) |
|
.join('-'); |
|
} |
|
|
|
simpleLocalDate(date) { |
|
const parts = date.slice(0, 10).split('-'); |
|
const localDate = new Date(parts[0], parts[1] - 1, parts[2]); |
|
localDate.setHours(0, 0, 0, 0); |
|
return localDate; |
|
} |
|
|
|
async getProjects() { |
|
const url = 'https://api.todoist.com/rest/v2/projects'; |
|
const response = await fetch(url, { |
|
headers: { |
|
Authorization: `Bearer ${Todoist.API_TOKEN()}`, |
|
}, |
|
}); |
|
const data = await response.json(); |
|
|
|
return data.reduce((acc, project) => { |
|
acc[project.id] = project; |
|
acc[project.id].tasks = {}; |
|
return acc; |
|
}, {}); |
|
} |
|
|
|
async getLabels() { |
|
const url = 'https://api.todoist.com/rest/v2/labels'; |
|
const response = await fetch(url, { |
|
headers: { |
|
Authorization: `Bearer ${Todoist.API_TOKEN()}`, |
|
}, |
|
}); |
|
const data = await response.json(); |
|
|
|
return data.reduce((acc, label) => { |
|
acc[label.name] = label; |
|
return acc; |
|
}, {}); |
|
} |
|
|
|
async getPreviousDueDatesForTask(taskID) { |
|
const url = |
|
'https://api.todoist.com/sync/v9/activity/get?' + |
|
new URLSearchParams({ |
|
object_id: taskID, |
|
object_type: 'item', |
|
event_type: 'updated', |
|
limit: 100, |
|
}); |
|
const response = await fetch(url, { |
|
headers: { |
|
Authorization: `Bearer ${Todoist.API_TOKEN()}`, |
|
}, |
|
}); |
|
const data = await response.json(); |
|
|
|
return data.events.reduce((acc, event) => { |
|
if (event.extra_data.last_due_date !== undefined) { |
|
const date = new Date(event.extra_data.last_due_date); |
|
date.setHours(0, 0, 0, 0); |
|
acc.push(date); |
|
} |
|
return acc; |
|
}, []); |
|
} |
|
|
|
async getItemInfo(taskID) { |
|
const url = |
|
'https://api.todoist.com/sync/v9/items/get?' + |
|
new URLSearchParams({ |
|
item_id: taskID, |
|
all_data: false, |
|
}); |
|
const response = await fetch(url, { |
|
headers: { |
|
Authorization: `Bearer ${Todoist.API_TOKEN()}`, |
|
}, |
|
}); |
|
const data = await response.json(); |
|
return data.item; |
|
} |
|
|
|
async getActiveTasks(date) { |
|
if (date === undefined) { |
|
date = new Date().toJSON().slice(0, 10); |
|
} |
|
|
|
const url = |
|
'https://api.todoist.com/rest/v2/tasks?' + |
|
new URLSearchParams({ |
|
filter: `(created before: ${date} | created: ${date}) & !no date`, |
|
}); |
|
const response = await fetch(url, { |
|
headers: { |
|
Authorization: `Bearer ${Todoist.API_TOKEN()}`, |
|
}, |
|
}); |
|
const data = await response.json(); |
|
const givenDate = this.simpleLocalDate(date); |
|
const givenDateString = givenDate.toUTCString(); |
|
|
|
return await data.reduce(async (acc, task) => { |
|
const dueDates = await this.getPreviousDueDatesForTask(task.id); |
|
const dueDate = this.simpleLocalDate(task.due.date); |
|
dueDates.unshift(dueDate); |
|
|
|
const index = dueDates.findIndex( |
|
(date) => date.toUTCString() === givenDateString, |
|
); |
|
|
|
if (index > -1) { |
|
task.postponed = dueDates.length - index - 1; |
|
(await acc).push(task); |
|
} |
|
return acc; |
|
}, []); |
|
} |
|
|
|
async getCompletedTasks(date) { |
|
if (date === undefined) { |
|
date = new Date().toJSON().slice(0, 10); |
|
} |
|
const givenDate = this.simpleLocalDate(date); |
|
const until = this.simpleLocalDate(date); |
|
until.setHours(23, 59); |
|
|
|
const url = |
|
'https://api.todoist.com/sync/v9/completed/get_all?' + |
|
new URLSearchParams({ |
|
since: givenDate.toJSON(), |
|
until: until.toJSON(), |
|
limit: 200, |
|
}); |
|
const response = await fetch(url, { |
|
headers: { |
|
Authorization: `Bearer ${Todoist.API_TOKEN()}`, |
|
}, |
|
}); |
|
const data = await response.json(); |
|
|
|
return await Promise.all( |
|
data.items.map(async (completedTask) => { |
|
return await this.getItemInfo(completedTask.task_id); |
|
}), |
|
); |
|
} |
|
|
|
async getTasksByProject(date) { |
|
if (date === undefined) { |
|
date = new Date().toJSON().slice(0, 10); |
|
} |
|
|
|
const active_tasks = await this.getActiveTasks(date); |
|
const completed_tasks = await this.getCompletedTasks(date); |
|
const projects = await this.getProjects(); |
|
|
|
active_tasks.forEach((task) => { |
|
projects[task.project_id].tasks[task.id] = task; |
|
}); |
|
|
|
completed_tasks.forEach((task) => { |
|
projects[task.project_id].tasks[task.id] = task; |
|
}); |
|
|
|
return Object.values(projects).sort((a, b) => (a.name > b.name ? 1 : -1)); |
|
} |
|
|
|
async getTaskMarkdown(date) { |
|
if (date === undefined) { |
|
date = new Date().toJSON().slice(0, 10); |
|
} |
|
const labels = await this.getLabels(); |
|
const projects = await this.getTasksByProject(date); |
|
|
|
return projects.reduce((acc, project) => { |
|
const tasks = Object.values(project.tasks).sort((a, b) => { |
|
if (a.content > b.content) { |
|
return 1; |
|
} |
|
if (a.content < b.content) { |
|
return -1; |
|
} |
|
if ( |
|
a.due !== undefined && |
|
a.due !== null && |
|
b.due !== undefined && |
|
b.due !== null |
|
) { |
|
return a.due.date > b.due.date ? 1 : -1; |
|
} |
|
return 0; |
|
}); |
|
|
|
if (tasks.length === 0) { |
|
return acc; |
|
} |
|
|
|
acc += `> [!todoist-${this.kebab(project.color)}-${this.kebab( |
|
project.name, |
|
)}]+ ${project.name}\n`; |
|
|
|
tasks.forEach((task) => { |
|
if (task.is_completed === false) { |
|
acc += `> - [ ]`; |
|
} else { |
|
acc += `> - [x]`; |
|
} |
|
acc += ` ${task.content}\n`; |
|
|
|
let extraParts = []; |
|
|
|
if (task.due !== undefined && task.due !== null) { |
|
let due = ''; |
|
|
|
if (task.due.datetime !== undefined) { |
|
const dueDate = new Date(task.due.datetime); |
|
due = dueDate.toLocaleTimeString([], { |
|
hour: '2-digit', |
|
minute: '2-digit', |
|
}); |
|
} else if (task.due.date !== undefined && task.due.date.length > 10) { |
|
const dueDate = new Date(task.due.date); |
|
due = dueDate.toLocaleTimeString([], { |
|
hour: '2-digit', |
|
minute: '2-digit', |
|
}); |
|
} |
|
|
|
if (due.length > 0 || task.due.is_recurring === true) { |
|
extraParts.push( |
|
`<span class="todoist-task-date${ |
|
task.due.is_recurring === true ? ' todoist-task-recurring' : '' |
|
}">${due}</span>`, |
|
); |
|
} |
|
} |
|
|
|
if (task.labels !== undefined && task.labels.length > 0) { |
|
extraParts = extraParts.concat( |
|
task.labels.map( |
|
(label) => |
|
`<span class="todoist-${this.kebab( |
|
labels[label].color, |
|
)}">${label}</span>`, |
|
), |
|
); |
|
} |
|
|
|
if (task.postponed !== undefined && task.postponed > 0) { |
|
extraParts.push( |
|
`<span class="todoist-postponed">${task.postponed}</span>`, |
|
); |
|
} |
|
|
|
if (extraParts.length > 0) { |
|
acc += `${extraParts |
|
.map((part) => { |
|
return `> - ${part}\n`; |
|
}) |
|
.join('')}`; |
|
} |
|
}); |
|
|
|
acc += '\n'; |
|
|
|
return acc; |
|
}, ''); |
|
} |
|
|
|
async Callout(dv) { |
|
const filename = dv.current().file.name; |
|
const markdown = await this.getTaskMarkdown(filename); |
|
dv.span(markdown); |
|
} |
|
} |
This is so useful, thanks so much for sharing!