Skip to content

Commit 855f2d4

Browse files
committed
Add Todoist and Google Tasks (wip, not tested)
1 parent 33d8061 commit 855f2d4

17 files changed

Lines changed: 1458 additions & 0 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@plotday/twister": minor
3+
---
4+
5+
Added: Todoist auth provider

connectors/google-tasks/LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2026 Plot Technologies Inc.
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

connectors/google-tasks/README.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# Google Tasks Connector for Plot
2+
3+
Sync your Google Tasks lists and tasks with Plot.
4+
5+
## Features
6+
7+
- OAuth authentication with Google
8+
- Sync tasks from Google Tasks lists to Plot
9+
- Periodic polling for updates (5-minute intervals)
10+
- Subtasks synced as notes with todo tags
11+
- Two-way completion status sync
12+
13+
## License
14+
15+
MIT
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
{
2+
"name": "@plotday/connector-google-tasks",
3+
"displayName": "Google Tasks",
4+
"description": "Tasks from Google Tasks",
5+
"logoUrl": "https://api.iconify.design/logos/google-tasks.svg",
6+
"author": "Plot <team@plot.day> (https://plot.day)",
7+
"license": "MIT",
8+
"version": "0.1.0",
9+
"type": "module",
10+
"main": "./dist/index.js",
11+
"types": "./dist/index.d.ts",
12+
"exports": {
13+
".": {
14+
"@plotday/connector": "./src/index.ts",
15+
"types": "./dist/index.d.ts",
16+
"default": "./dist/index.js"
17+
}
18+
},
19+
"files": ["dist", "README.md", "LICENSE"],
20+
"scripts": {
21+
"build": "tsc",
22+
"clean": "rm -rf dist"
23+
},
24+
"dependencies": {
25+
"@plotday/twister": "workspace:^"
26+
},
27+
"devDependencies": {
28+
"typescript": "^5.9.3"
29+
},
30+
"repository": {
31+
"type": "git",
32+
"url": "https://github.com/plotday/plot.git",
33+
"directory": "connectors/google-tasks"
34+
},
35+
"homepage": "https://plot.day",
36+
"keywords": ["plot", "connector", "google-tasks"],
37+
"publishConfig": { "access": "public" }
38+
}

connectors/google-tasks/src/api.ts

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
/**
2+
* Google Tasks API client helpers.
3+
*
4+
* Uses the REST API directly since there's no official SDK for Workers.
5+
* https://developers.google.com/tasks/reference/rest
6+
*/
7+
8+
const BASE_URL = "https://tasks.googleapis.com/tasks/v1";
9+
10+
export type GoogleTaskList = {
11+
id: string;
12+
title: string;
13+
updated: string;
14+
selfLink: string;
15+
};
16+
17+
export type GoogleTask = {
18+
id: string;
19+
title: string;
20+
notes?: string;
21+
status: "needsAction" | "completed";
22+
due?: string;
23+
completed?: string;
24+
updated: string;
25+
parent?: string;
26+
position: string;
27+
selfLink: string;
28+
webViewLink?: string;
29+
links?: Array<{ type: string; description: string; link: string }>;
30+
};
31+
32+
type TaskListResponse = {
33+
items?: GoogleTaskList[];
34+
nextPageToken?: string;
35+
};
36+
37+
type TasksResponse = {
38+
items?: GoogleTask[];
39+
nextPageToken?: string;
40+
};
41+
42+
async function request<T>(
43+
token: string,
44+
path: string,
45+
options?: RequestInit
46+
): Promise<T> {
47+
const response = await fetch(`${BASE_URL}${path}`, {
48+
...options,
49+
headers: {
50+
Authorization: `Bearer ${token}`,
51+
"Content-Type": "application/json",
52+
...options?.headers,
53+
},
54+
});
55+
if (!response.ok) {
56+
const text = await response.text();
57+
throw new Error(
58+
`Google Tasks API error ${response.status}: ${text}`
59+
);
60+
}
61+
return response.json() as Promise<T>;
62+
}
63+
64+
/**
65+
* List all task lists for the authenticated user.
66+
*/
67+
export async function listTaskLists(
68+
token: string
69+
): Promise<GoogleTaskList[]> {
70+
const allLists: GoogleTaskList[] = [];
71+
let pageToken: string | undefined;
72+
73+
do {
74+
const params = new URLSearchParams({ maxResults: "100" });
75+
if (pageToken) params.set("pageToken", pageToken);
76+
77+
const result = await request<TaskListResponse>(
78+
token,
79+
`/users/@me/lists?${params}`
80+
);
81+
if (result.items) {
82+
allLists.push(...result.items);
83+
}
84+
pageToken = result.nextPageToken;
85+
} while (pageToken);
86+
87+
return allLists;
88+
}
89+
90+
/**
91+
* List tasks in a task list.
92+
*/
93+
export async function listTasks(
94+
token: string,
95+
listId: string,
96+
options?: {
97+
showCompleted?: boolean;
98+
updatedMin?: string;
99+
pageToken?: string;
100+
maxResults?: number;
101+
}
102+
): Promise<{ tasks: GoogleTask[]; nextPageToken?: string }> {
103+
const params = new URLSearchParams({
104+
maxResults: String(options?.maxResults ?? 50),
105+
});
106+
if (options?.showCompleted !== undefined) {
107+
params.set("showCompleted", String(options.showCompleted));
108+
if (options.showCompleted) {
109+
params.set("showHidden", "true");
110+
}
111+
}
112+
if (options?.updatedMin) {
113+
params.set("updatedMin", options.updatedMin);
114+
// When using updatedMin, must show completed to get status changes
115+
params.set("showCompleted", "true");
116+
params.set("showHidden", "true");
117+
}
118+
if (options?.pageToken) {
119+
params.set("pageToken", options.pageToken);
120+
}
121+
122+
const result = await request<TasksResponse>(
123+
token,
124+
`/lists/${encodeURIComponent(listId)}/tasks?${params}`
125+
);
126+
127+
return {
128+
tasks: result.items ?? [],
129+
nextPageToken: result.nextPageToken,
130+
};
131+
}
132+
133+
/**
134+
* Update a task's status.
135+
*/
136+
export async function updateTask(
137+
token: string,
138+
listId: string,
139+
taskId: string,
140+
updates: { status?: "needsAction" | "completed" }
141+
): Promise<GoogleTask> {
142+
return request<GoogleTask>(
143+
token,
144+
`/lists/${encodeURIComponent(listId)}/tasks/${encodeURIComponent(taskId)}`,
145+
{
146+
method: "PATCH",
147+
body: JSON.stringify(updates),
148+
}
149+
);
150+
}

0 commit comments

Comments
 (0)