TaskDecay v1.0.0 MVP — smart task review & archive Chrome extension
This commit is contained in:
@@ -0,0 +1,63 @@
|
||||
import { fetchTodoistTasks, closeTodoistTask, deleteTodoistTask } from './todoist.js';
|
||||
import { fetchTickTickTasks, closeTickTickTask, deleteTickTickTask } from './ticktick.js';
|
||||
import { fetchNotionTasks, archiveNotionTask, deleteNotionTask } from './notion.js';
|
||||
export async function fetchAllTasks(providers) {
|
||||
const all = [];
|
||||
const errors = [];
|
||||
if (providers.todoist.enabled && providers.todoist.token) {
|
||||
try {
|
||||
const tasks = await fetchTodoistTasks(providers.todoist);
|
||||
all.push(...tasks);
|
||||
}
|
||||
catch (e) {
|
||||
errors.push(`Todoist: ${e.message}`);
|
||||
}
|
||||
}
|
||||
if (providers.ticktick.enabled && providers.ticktick.token) {
|
||||
try {
|
||||
const tasks = await fetchTickTickTasks(providers.ticktick);
|
||||
all.push(...tasks);
|
||||
}
|
||||
catch (e) {
|
||||
errors.push(`TickTick: ${e.message}`);
|
||||
}
|
||||
}
|
||||
if (providers.notion.enabled && providers.notion.token) {
|
||||
try {
|
||||
const tasks = await fetchNotionTasks(providers.notion);
|
||||
all.push(...tasks);
|
||||
}
|
||||
catch (e) {
|
||||
errors.push(`Notion: ${e.message}`);
|
||||
}
|
||||
}
|
||||
if (errors.length > 0 && all.length === 0) {
|
||||
throw new Error(errors.join('; '));
|
||||
}
|
||||
return all;
|
||||
}
|
||||
export async function completeTask(task, token) {
|
||||
if (task.source === 'todoist')
|
||||
await closeTodoistTask(task.id, token);
|
||||
else if (task.source === 'ticktick')
|
||||
await closeTickTickTask(task.id, token);
|
||||
else if (task.source === 'notion')
|
||||
await archiveNotionTask(task.id, token);
|
||||
}
|
||||
export async function deleteTask(task, token) {
|
||||
if (task.source === 'todoist')
|
||||
await deleteTodoistTask(task.id, token);
|
||||
else if (task.source === 'ticktick')
|
||||
await deleteTickTickTask(task.id, token);
|
||||
else if (task.source === 'notion')
|
||||
await deleteNotionTask(task.id, token);
|
||||
}
|
||||
export function getTokenForSource(source, providers) {
|
||||
if (source === 'todoist')
|
||||
return providers.todoist.token;
|
||||
if (source === 'ticktick')
|
||||
return providers.ticktick.token;
|
||||
if (source === 'notion')
|
||||
return providers.notion.token;
|
||||
return '';
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
// Notion API — queries a database for tasks
|
||||
export async function fetchNotionTasks(config) {
|
||||
if (!config.token)
|
||||
throw new Error('Notion token not configured');
|
||||
if (!config.projectIds || config.projectIds.length === 0) {
|
||||
throw new Error('Notion database ID not configured. Set a database ID in options.');
|
||||
}
|
||||
const headers = {
|
||||
'Authorization': `Bearer ${config.token}`,
|
||||
'Notion-Version': '2022-06-28',
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
const allTasks = [];
|
||||
for (const dbId of config.projectIds) {
|
||||
const res = await fetch(`https://api.notion.com/v1/databases/${dbId}/query`, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify({ page_size: 100 }),
|
||||
});
|
||||
if (!res.ok) {
|
||||
console.warn(`Notion DB query failed for ${dbId}: ${res.status}`);
|
||||
continue;
|
||||
}
|
||||
const data = await res.json();
|
||||
const results = data.results || [];
|
||||
for (const page of results) {
|
||||
const props = page.properties || {};
|
||||
const titleProp = Object.values(props).find((p) => p.type === 'title');
|
||||
const title = titleProp?.title?.[0]?.plain_text || 'Untitled';
|
||||
const statusProp = Object.values(props).find((p) => p.type === 'status' || p.type === 'select' || p.type === 'checkbox');
|
||||
let done = false;
|
||||
if (statusProp?.type === 'checkbox')
|
||||
done = statusProp.checkbox;
|
||||
else if (statusProp?.type === 'status')
|
||||
done = statusProp.status?.name === 'Done';
|
||||
else if (statusProp?.type === 'select')
|
||||
done = statusProp.select?.name === 'Done';
|
||||
if (done)
|
||||
continue;
|
||||
const dateProp = Object.values(props).find((p) => p.type === 'date');
|
||||
const dueDate = dateProp?.date?.start ? new Date(dateProp.date.start) : null;
|
||||
const multiSelect = Object.values(props).filter((p) => p.type === 'multi_select');
|
||||
const labels = multiSelect.flatMap((p) => p.multi_select?.map((s) => s.name) || []);
|
||||
const created = page.created_time ? new Date(page.created_time) : new Date();
|
||||
const updated = page.last_edited_time ? new Date(page.last_edited_time) : created;
|
||||
allTasks.push({
|
||||
id: `notion-${page.id}`,
|
||||
title,
|
||||
priority: 1,
|
||||
createdAt: created,
|
||||
updatedAt: updated,
|
||||
completedAt: null,
|
||||
dueDate,
|
||||
projectId: `notion-${dbId}`,
|
||||
projectName: dbId.slice(0, 8),
|
||||
labels,
|
||||
url: page.url,
|
||||
source: 'notion',
|
||||
raw: page,
|
||||
});
|
||||
}
|
||||
}
|
||||
return allTasks;
|
||||
}
|
||||
export async function archiveNotionTask(taskId, token) {
|
||||
const realId = taskId.replace('notion-', '');
|
||||
const res = await fetch(`https://api.notion.com/v1/pages/${realId}`, {
|
||||
method: 'PATCH',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Notion-Version': '2022-06-28',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ archived: true }),
|
||||
});
|
||||
if (!res.ok)
|
||||
throw new Error(`Failed to archive Notion task: ${res.status}`);
|
||||
}
|
||||
export async function deleteNotionTask(taskId, token) {
|
||||
const realId = taskId.replace('notion-', '');
|
||||
const res = await fetch(`https://api.notion.com/v1/blocks/${realId}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Notion-Version': '2022-06-28',
|
||||
},
|
||||
});
|
||||
if (!res.ok)
|
||||
throw new Error(`Failed to delete Notion task: ${res.status}`);
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
// TickTick API v2 — uses login token or OAuth access token
|
||||
export async function fetchTickTickTasks(config) {
|
||||
if (!config.token)
|
||||
throw new Error('TickTick token not configured');
|
||||
const headers = {
|
||||
'Authorization': `Bearer ${config.token}`,
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
// Get all projects first
|
||||
const projectsRes = await fetch('https://api.ticktick.com/open/v1/project', { headers });
|
||||
const projects = projectsRes.ok ? await projectsRes.json() : [];
|
||||
const projectList = Array.isArray(projects) ? projects : (projects || []);
|
||||
const allTasks = [];
|
||||
for (const proj of projectList) {
|
||||
const pid = proj.id;
|
||||
const tasksRes = await fetch(`https://api.ticktick.com/open/v1/project/${pid}/task`, { headers });
|
||||
if (!tasksRes.ok)
|
||||
continue;
|
||||
const tasks = await tasksRes.json();
|
||||
const taskList = Array.isArray(tasks) ? tasks : (tasks || []);
|
||||
for (const t of taskList) {
|
||||
if (t.status === 2)
|
||||
continue; // completed
|
||||
allTasks.push({
|
||||
id: `ticktick-${t.id}`,
|
||||
title: t.title,
|
||||
priority: (t.priority || 0) + 1,
|
||||
createdAt: new Date(t.createdTime || Date.now()),
|
||||
updatedAt: new Date(t.modifiedTime || t.createdTime || Date.now()),
|
||||
completedAt: null,
|
||||
dueDate: t.dueDate ? new Date(t.dueDate) : null,
|
||||
projectId: `ticktick-${pid}`,
|
||||
projectName: proj.name || null,
|
||||
labels: t.tags || [],
|
||||
url: `https://ticktick.com/webapp/#p/${pid}/task/${t.id}`,
|
||||
source: 'ticktick',
|
||||
raw: t,
|
||||
});
|
||||
}
|
||||
}
|
||||
return allTasks;
|
||||
}
|
||||
export async function closeTickTickTask(taskId, token) {
|
||||
const realId = taskId.replace('ticktick-', '');
|
||||
const res = await fetch(`https://api.ticktick.com/open/v1/task/${realId}/complete`, {
|
||||
method: 'POST',
|
||||
headers: { 'Authorization': `Bearer ${token}` },
|
||||
});
|
||||
if (!res.ok)
|
||||
throw new Error(`Failed to close TickTick task: ${res.status}`);
|
||||
}
|
||||
export async function deleteTickTickTask(taskId, token) {
|
||||
const realId = taskId.replace('ticktick-', '');
|
||||
const res = await fetch(`https://api.ticktick.com/open/v1/task/${realId}`, {
|
||||
method: 'DELETE',
|
||||
headers: { 'Authorization': `Bearer ${token}` },
|
||||
});
|
||||
if (!res.ok)
|
||||
throw new Error(`Failed to delete TickTick task: ${res.status}`);
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
export async function fetchTodoistTasks(config) {
|
||||
if (!config.token)
|
||||
throw new Error('Todoist token not configured');
|
||||
const headers = {
|
||||
'Authorization': `Bearer ${config.token}`,
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
// Fetch all active tasks
|
||||
const tasksRes = await fetch('https://api.todoist.com/rest/v2/tasks', { headers });
|
||||
if (!tasksRes.ok) {
|
||||
throw new Error(`Todoist API error: ${tasksRes.status} ${tasksRes.statusText}`);
|
||||
}
|
||||
const tasks = await tasksRes.json();
|
||||
// Fetch projects for names
|
||||
const projectsRes = await fetch('https://api.todoist.com/rest/v2/projects', { headers });
|
||||
const projects = projectsRes.ok ? await projectsRes.json() : [];
|
||||
const projectMap = new Map(projects.map((p) => [p.id, p.name]));
|
||||
return tasks.map((t) => ({
|
||||
id: `todoist-${t.id}`,
|
||||
title: t.content,
|
||||
priority: t.priority || 1,
|
||||
createdAt: new Date(t.created_at || Date.now()),
|
||||
updatedAt: new Date(t.updated_at || t.created_at || Date.now()),
|
||||
completedAt: null,
|
||||
dueDate: t.due?.date ? new Date(t.due.date) : null,
|
||||
projectId: t.project_id ? `todoist-${t.project_id}` : null,
|
||||
projectName: projectMap.get(t.project_id) || null,
|
||||
labels: t.labels || [],
|
||||
url: `https://todoist.com/app/task/${t.id}`,
|
||||
source: 'todoist',
|
||||
raw: t,
|
||||
}));
|
||||
}
|
||||
export async function closeTodoistTask(taskId, token) {
|
||||
const realId = taskId.replace('todoist-', '');
|
||||
const res = await fetch(`https://api.todoist.com/rest/v2/tasks/${realId}/close`, {
|
||||
method: 'POST',
|
||||
headers: { 'Authorization': `Bearer ${token}` },
|
||||
});
|
||||
if (!res.ok)
|
||||
throw new Error(`Failed to close Todoist task: ${res.status}`);
|
||||
}
|
||||
export async function deleteTodoistTask(taskId, token) {
|
||||
const realId = taskId.replace('todoist-', '');
|
||||
const res = await fetch(`https://api.todoist.com/rest/v2/tasks/${realId}`, {
|
||||
method: 'DELETE',
|
||||
headers: { 'Authorization': `Bearer ${token}` },
|
||||
});
|
||||
if (!res.ok)
|
||||
throw new Error(`Failed to delete Todoist task: ${res.status}`);
|
||||
}
|
||||
Reference in New Issue
Block a user