@auxiora/connector-google-workspace 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +191 -0
- package/dist/connector.d.ts +2 -0
- package/dist/connector.d.ts.map +1 -0
- package/dist/connector.js +630 -0
- package/dist/connector.js.map +1 -0
- package/dist/google-client.d.ts +7 -0
- package/dist/google-client.d.ts.map +1 -0
- package/dist/google-client.js +11 -0
- package/dist/google-client.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/package.json +26 -0
- package/src/connector.ts +667 -0
- package/src/google-client.ts +13 -0
- package/src/index.ts +1 -0
- package/tests/connector.test.ts +553 -0
- package/tsconfig.json +11 -0
- package/tsconfig.tsbuildinfo +1 -0
package/src/connector.ts
ADDED
|
@@ -0,0 +1,667 @@
|
|
|
1
|
+
import { Readable } from 'node:stream';
|
|
2
|
+
import { defineConnector } from '@auxiora/connectors';
|
|
3
|
+
import type { TriggerEvent } from '@auxiora/connectors';
|
|
4
|
+
import { createGoogleClient } from './google-client.js';
|
|
5
|
+
|
|
6
|
+
/** Decode a base64url-encoded string to UTF-8. */
|
|
7
|
+
function base64urlDecode(data: string): string {
|
|
8
|
+
return Buffer.from(data, 'base64url').toString('utf-8');
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** Extract the plain-text body from a Gmail message payload. */
|
|
12
|
+
function extractBody(payload: { mimeType?: string | null; body?: { data?: string | null } | null; parts?: Array<{ mimeType?: string | null; body?: { data?: string | null } | null; parts?: unknown[] }> | null } | null | undefined): string {
|
|
13
|
+
if (!payload) return '';
|
|
14
|
+
|
|
15
|
+
// Direct body on simple messages
|
|
16
|
+
if (payload.mimeType === 'text/plain' && payload.body?.data) {
|
|
17
|
+
return base64urlDecode(payload.body.data);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Multipart: look for text/plain first, then text/html
|
|
21
|
+
if (payload.parts) {
|
|
22
|
+
const plainPart = payload.parts.find(p => p.mimeType === 'text/plain');
|
|
23
|
+
if (plainPart?.body?.data) {
|
|
24
|
+
return base64urlDecode(plainPart.body.data);
|
|
25
|
+
}
|
|
26
|
+
const htmlPart = payload.parts.find(p => p.mimeType === 'text/html');
|
|
27
|
+
if (htmlPart?.body?.data) {
|
|
28
|
+
return base64urlDecode(htmlPart.body.data);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Fallback: any body data
|
|
33
|
+
if (payload.body?.data) {
|
|
34
|
+
return base64urlDecode(payload.body.data);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return '';
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export const googleWorkspaceConnector = defineConnector({
|
|
41
|
+
id: 'google-workspace',
|
|
42
|
+
name: 'Google Workspace',
|
|
43
|
+
description: 'Integration with Google Calendar, Gmail, and Drive',
|
|
44
|
+
version: '1.0.0',
|
|
45
|
+
category: 'productivity',
|
|
46
|
+
icon: 'google',
|
|
47
|
+
|
|
48
|
+
auth: {
|
|
49
|
+
type: 'oauth2',
|
|
50
|
+
oauth2: {
|
|
51
|
+
authUrl: 'https://accounts.google.com/o/oauth2/v2/auth',
|
|
52
|
+
tokenUrl: 'https://oauth2.googleapis.com/token',
|
|
53
|
+
scopes: [
|
|
54
|
+
'https://www.googleapis.com/auth/calendar',
|
|
55
|
+
'https://www.googleapis.com/auth/gmail.modify',
|
|
56
|
+
'https://www.googleapis.com/auth/drive',
|
|
57
|
+
],
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
|
|
61
|
+
actions: [
|
|
62
|
+
// --- Calendar ---
|
|
63
|
+
{
|
|
64
|
+
id: 'calendar-list-events',
|
|
65
|
+
name: 'List Calendar Events',
|
|
66
|
+
description: 'List upcoming events from Google Calendar',
|
|
67
|
+
trustMinimum: 1,
|
|
68
|
+
trustDomain: 'calendar',
|
|
69
|
+
reversible: false,
|
|
70
|
+
sideEffects: false,
|
|
71
|
+
params: {
|
|
72
|
+
calendarId: { type: 'string', description: 'Calendar ID (default: primary)', default: 'primary' },
|
|
73
|
+
maxResults: { type: 'number', description: 'Max events to return', default: 10 },
|
|
74
|
+
timeMin: { type: 'string', description: 'Start time (ISO 8601)' },
|
|
75
|
+
timeMax: { type: 'string', description: 'End time (ISO 8601)' },
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
id: 'calendar-create-event',
|
|
80
|
+
name: 'Create Calendar Event',
|
|
81
|
+
description: 'Create a new event in Google Calendar',
|
|
82
|
+
trustMinimum: 2,
|
|
83
|
+
trustDomain: 'calendar',
|
|
84
|
+
reversible: true,
|
|
85
|
+
sideEffects: true,
|
|
86
|
+
params: {
|
|
87
|
+
summary: { type: 'string', description: 'Event title', required: true },
|
|
88
|
+
start: { type: 'string', description: 'Start time (ISO 8601)', required: true },
|
|
89
|
+
end: { type: 'string', description: 'End time (ISO 8601)', required: true },
|
|
90
|
+
description: { type: 'string', description: 'Event description' },
|
|
91
|
+
attendees: { type: 'array', description: 'List of attendee emails' },
|
|
92
|
+
calendarId: { type: 'string', description: 'Calendar ID', default: 'primary' },
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
id: 'calendar-update-event',
|
|
97
|
+
name: 'Update Calendar Event',
|
|
98
|
+
description: 'Update an existing calendar event',
|
|
99
|
+
trustMinimum: 2,
|
|
100
|
+
trustDomain: 'calendar',
|
|
101
|
+
reversible: true,
|
|
102
|
+
sideEffects: true,
|
|
103
|
+
params: {
|
|
104
|
+
eventId: { type: 'string', description: 'Event ID', required: true },
|
|
105
|
+
summary: { type: 'string', description: 'Updated title' },
|
|
106
|
+
start: { type: 'string', description: 'Updated start time' },
|
|
107
|
+
end: { type: 'string', description: 'Updated end time' },
|
|
108
|
+
calendarId: { type: 'string', description: 'Calendar ID', default: 'primary' },
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
id: 'calendar-delete-event',
|
|
113
|
+
name: 'Delete Calendar Event',
|
|
114
|
+
description: 'Delete a calendar event',
|
|
115
|
+
trustMinimum: 3,
|
|
116
|
+
trustDomain: 'calendar',
|
|
117
|
+
reversible: false,
|
|
118
|
+
sideEffects: true,
|
|
119
|
+
params: {
|
|
120
|
+
eventId: { type: 'string', description: 'Event ID', required: true },
|
|
121
|
+
calendarId: { type: 'string', description: 'Calendar ID', default: 'primary' },
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
id: 'calendar-find-free-slots',
|
|
126
|
+
name: 'Find Free Slots',
|
|
127
|
+
description: 'Find free time slots across calendars',
|
|
128
|
+
trustMinimum: 1,
|
|
129
|
+
trustDomain: 'calendar',
|
|
130
|
+
reversible: false,
|
|
131
|
+
sideEffects: false,
|
|
132
|
+
params: {
|
|
133
|
+
timeMin: { type: 'string', description: 'Start of window (ISO 8601)', required: true },
|
|
134
|
+
timeMax: { type: 'string', description: 'End of window (ISO 8601)', required: true },
|
|
135
|
+
calendarIds: { type: 'array', description: 'Calendar IDs to check' },
|
|
136
|
+
durationMinutes: { type: 'number', description: 'Desired slot duration in minutes', default: 30 },
|
|
137
|
+
},
|
|
138
|
+
},
|
|
139
|
+
// --- Gmail ---
|
|
140
|
+
{
|
|
141
|
+
id: 'gmail-list-messages',
|
|
142
|
+
name: 'List Gmail Messages',
|
|
143
|
+
description: 'List recent emails from Gmail',
|
|
144
|
+
trustMinimum: 1,
|
|
145
|
+
trustDomain: 'email',
|
|
146
|
+
reversible: false,
|
|
147
|
+
sideEffects: false,
|
|
148
|
+
params: {
|
|
149
|
+
maxResults: { type: 'number', description: 'Max messages to return', default: 10 },
|
|
150
|
+
query: { type: 'string', description: 'Gmail search query' },
|
|
151
|
+
labelIds: { type: 'array', description: 'Label IDs to filter by' },
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
id: 'gmail-read-message',
|
|
156
|
+
name: 'Read Gmail Message',
|
|
157
|
+
description: 'Read a specific email message',
|
|
158
|
+
trustMinimum: 1,
|
|
159
|
+
trustDomain: 'email',
|
|
160
|
+
reversible: false,
|
|
161
|
+
sideEffects: false,
|
|
162
|
+
params: {
|
|
163
|
+
messageId: { type: 'string', description: 'Message ID', required: true },
|
|
164
|
+
},
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
id: 'gmail-send',
|
|
168
|
+
name: 'Send Email',
|
|
169
|
+
description: 'Send an email via Gmail',
|
|
170
|
+
trustMinimum: 3,
|
|
171
|
+
trustDomain: 'email',
|
|
172
|
+
reversible: false,
|
|
173
|
+
sideEffects: true,
|
|
174
|
+
params: {
|
|
175
|
+
to: { type: 'string', description: 'Recipient email', required: true },
|
|
176
|
+
subject: { type: 'string', description: 'Email subject', required: true },
|
|
177
|
+
body: { type: 'string', description: 'Email body', required: true },
|
|
178
|
+
cc: { type: 'string', description: 'CC recipients' },
|
|
179
|
+
bcc: { type: 'string', description: 'BCC recipients' },
|
|
180
|
+
},
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
id: 'gmail-draft',
|
|
184
|
+
name: 'Create Draft',
|
|
185
|
+
description: 'Create a draft email in Gmail',
|
|
186
|
+
trustMinimum: 2,
|
|
187
|
+
trustDomain: 'email',
|
|
188
|
+
reversible: true,
|
|
189
|
+
sideEffects: true,
|
|
190
|
+
params: {
|
|
191
|
+
to: { type: 'string', description: 'Recipient email', required: true },
|
|
192
|
+
subject: { type: 'string', description: 'Email subject', required: true },
|
|
193
|
+
body: { type: 'string', description: 'Email body', required: true },
|
|
194
|
+
},
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
id: 'gmail-search',
|
|
198
|
+
name: 'Search Gmail',
|
|
199
|
+
description: 'Search emails in Gmail',
|
|
200
|
+
trustMinimum: 1,
|
|
201
|
+
trustDomain: 'email',
|
|
202
|
+
reversible: false,
|
|
203
|
+
sideEffects: false,
|
|
204
|
+
params: {
|
|
205
|
+
query: { type: 'string', description: 'Search query', required: true },
|
|
206
|
+
maxResults: { type: 'number', description: 'Max results', default: 10 },
|
|
207
|
+
},
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
id: 'gmail-archive',
|
|
211
|
+
name: 'Archive Email',
|
|
212
|
+
description: 'Archive a Gmail message',
|
|
213
|
+
trustMinimum: 2,
|
|
214
|
+
trustDomain: 'email',
|
|
215
|
+
reversible: true,
|
|
216
|
+
sideEffects: true,
|
|
217
|
+
params: {
|
|
218
|
+
messageId: { type: 'string', description: 'Message ID', required: true },
|
|
219
|
+
},
|
|
220
|
+
},
|
|
221
|
+
// --- Drive ---
|
|
222
|
+
{
|
|
223
|
+
id: 'drive-list-files',
|
|
224
|
+
name: 'List Drive Files',
|
|
225
|
+
description: 'List files in Google Drive',
|
|
226
|
+
trustMinimum: 1,
|
|
227
|
+
trustDomain: 'files',
|
|
228
|
+
reversible: false,
|
|
229
|
+
sideEffects: false,
|
|
230
|
+
params: {
|
|
231
|
+
folderId: { type: 'string', description: 'Folder ID (default: root)' },
|
|
232
|
+
maxResults: { type: 'number', description: 'Max files to return', default: 20 },
|
|
233
|
+
query: { type: 'string', description: 'Drive search query' },
|
|
234
|
+
},
|
|
235
|
+
},
|
|
236
|
+
{
|
|
237
|
+
id: 'drive-read-file',
|
|
238
|
+
name: 'Read Drive File',
|
|
239
|
+
description: 'Read a file from Google Drive',
|
|
240
|
+
trustMinimum: 1,
|
|
241
|
+
trustDomain: 'files',
|
|
242
|
+
reversible: false,
|
|
243
|
+
sideEffects: false,
|
|
244
|
+
params: {
|
|
245
|
+
fileId: { type: 'string', description: 'File ID', required: true },
|
|
246
|
+
},
|
|
247
|
+
},
|
|
248
|
+
{
|
|
249
|
+
id: 'drive-create-file',
|
|
250
|
+
name: 'Create Drive File',
|
|
251
|
+
description: 'Create a new file in Google Drive',
|
|
252
|
+
trustMinimum: 2,
|
|
253
|
+
trustDomain: 'files',
|
|
254
|
+
reversible: true,
|
|
255
|
+
sideEffects: true,
|
|
256
|
+
params: {
|
|
257
|
+
name: { type: 'string', description: 'File name', required: true },
|
|
258
|
+
content: { type: 'string', description: 'File content', required: true },
|
|
259
|
+
mimeType: { type: 'string', description: 'MIME type', default: 'text/plain' },
|
|
260
|
+
folderId: { type: 'string', description: 'Parent folder ID' },
|
|
261
|
+
},
|
|
262
|
+
},
|
|
263
|
+
{
|
|
264
|
+
id: 'drive-upload',
|
|
265
|
+
name: 'Upload to Drive',
|
|
266
|
+
description: 'Upload a file to Google Drive',
|
|
267
|
+
trustMinimum: 2,
|
|
268
|
+
trustDomain: 'files',
|
|
269
|
+
reversible: true,
|
|
270
|
+
sideEffects: true,
|
|
271
|
+
params: {
|
|
272
|
+
name: { type: 'string', description: 'File name', required: true },
|
|
273
|
+
content: { type: 'string', description: 'Base64-encoded content', required: true },
|
|
274
|
+
mimeType: { type: 'string', description: 'MIME type', required: true },
|
|
275
|
+
folderId: { type: 'string', description: 'Parent folder ID' },
|
|
276
|
+
},
|
|
277
|
+
},
|
|
278
|
+
{
|
|
279
|
+
id: 'drive-search',
|
|
280
|
+
name: 'Search Drive',
|
|
281
|
+
description: 'Search files in Google Drive',
|
|
282
|
+
trustMinimum: 1,
|
|
283
|
+
trustDomain: 'files',
|
|
284
|
+
reversible: false,
|
|
285
|
+
sideEffects: false,
|
|
286
|
+
params: {
|
|
287
|
+
query: { type: 'string', description: 'Search query', required: true },
|
|
288
|
+
maxResults: { type: 'number', description: 'Max results', default: 10 },
|
|
289
|
+
},
|
|
290
|
+
},
|
|
291
|
+
{
|
|
292
|
+
id: 'drive-share',
|
|
293
|
+
name: 'Share Drive File',
|
|
294
|
+
description: 'Share a Google Drive file with someone',
|
|
295
|
+
trustMinimum: 3,
|
|
296
|
+
trustDomain: 'files',
|
|
297
|
+
reversible: true,
|
|
298
|
+
sideEffects: true,
|
|
299
|
+
params: {
|
|
300
|
+
fileId: { type: 'string', description: 'File ID', required: true },
|
|
301
|
+
email: { type: 'string', description: 'Email to share with', required: true },
|
|
302
|
+
role: { type: 'string', description: 'Permission role (reader, writer, commenter)', default: 'reader' },
|
|
303
|
+
},
|
|
304
|
+
},
|
|
305
|
+
],
|
|
306
|
+
|
|
307
|
+
triggers: [
|
|
308
|
+
{
|
|
309
|
+
id: 'new-email',
|
|
310
|
+
name: 'New Email',
|
|
311
|
+
description: 'Triggered when a new email arrives',
|
|
312
|
+
type: 'poll',
|
|
313
|
+
pollIntervalMs: 60_000,
|
|
314
|
+
},
|
|
315
|
+
{
|
|
316
|
+
id: 'event-starting-soon',
|
|
317
|
+
name: 'Event Starting Soon',
|
|
318
|
+
description: 'Triggered when a calendar event is about to start',
|
|
319
|
+
type: 'poll',
|
|
320
|
+
pollIntervalMs: 60_000,
|
|
321
|
+
},
|
|
322
|
+
{
|
|
323
|
+
id: 'file-shared',
|
|
324
|
+
name: 'File Shared',
|
|
325
|
+
description: 'Triggered when a file is shared with you',
|
|
326
|
+
type: 'poll',
|
|
327
|
+
pollIntervalMs: 300_000,
|
|
328
|
+
},
|
|
329
|
+
],
|
|
330
|
+
|
|
331
|
+
entities: [
|
|
332
|
+
{
|
|
333
|
+
id: 'calendar-event',
|
|
334
|
+
name: 'Calendar Event',
|
|
335
|
+
description: 'A Google Calendar event',
|
|
336
|
+
fields: { id: 'string', summary: 'string', start: 'string', end: 'string', attendees: 'array' },
|
|
337
|
+
},
|
|
338
|
+
{
|
|
339
|
+
id: 'email-message',
|
|
340
|
+
name: 'Email Message',
|
|
341
|
+
description: 'A Gmail message',
|
|
342
|
+
fields: { id: 'string', from: 'string', to: 'string', subject: 'string', body: 'string', date: 'string' },
|
|
343
|
+
},
|
|
344
|
+
{
|
|
345
|
+
id: 'drive-file',
|
|
346
|
+
name: 'Drive File',
|
|
347
|
+
description: 'A Google Drive file',
|
|
348
|
+
fields: { id: 'string', name: 'string', mimeType: 'string', size: 'number', modifiedTime: 'string' },
|
|
349
|
+
},
|
|
350
|
+
],
|
|
351
|
+
|
|
352
|
+
async executeAction(actionId: string, params: Record<string, unknown>, token: string): Promise<unknown> {
|
|
353
|
+
const client = createGoogleClient(token);
|
|
354
|
+
|
|
355
|
+
switch (actionId) {
|
|
356
|
+
// --- Calendar ---
|
|
357
|
+
case 'calendar-list-events': {
|
|
358
|
+
const res = await client.calendar.events.list({
|
|
359
|
+
calendarId: (params.calendarId as string) ?? 'primary',
|
|
360
|
+
maxResults: (params.maxResults as number) ?? 10,
|
|
361
|
+
timeMin: (params.timeMin as string) ?? new Date().toISOString(),
|
|
362
|
+
timeMax: params.timeMax as string | undefined,
|
|
363
|
+
singleEvents: true,
|
|
364
|
+
orderBy: 'startTime',
|
|
365
|
+
});
|
|
366
|
+
return { events: res.data.items ?? [] };
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
case 'calendar-create-event': {
|
|
370
|
+
const attendees = params.attendees
|
|
371
|
+
? (params.attendees as string[]).map(email => ({ email }))
|
|
372
|
+
: undefined;
|
|
373
|
+
const res = await client.calendar.events.insert({
|
|
374
|
+
calendarId: (params.calendarId as string) ?? 'primary',
|
|
375
|
+
requestBody: {
|
|
376
|
+
summary: params.summary as string,
|
|
377
|
+
description: params.description as string | undefined,
|
|
378
|
+
start: { dateTime: params.start as string },
|
|
379
|
+
end: { dateTime: params.end as string },
|
|
380
|
+
attendees,
|
|
381
|
+
},
|
|
382
|
+
});
|
|
383
|
+
return { eventId: res.data.id, status: 'created', summary: res.data.summary };
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
case 'calendar-update-event': {
|
|
387
|
+
const requestBody: Record<string, unknown> = {};
|
|
388
|
+
if (params.summary) requestBody.summary = params.summary;
|
|
389
|
+
if (params.start) requestBody.start = { dateTime: params.start as string };
|
|
390
|
+
if (params.end) requestBody.end = { dateTime: params.end as string };
|
|
391
|
+
const res = await client.calendar.events.patch({
|
|
392
|
+
calendarId: (params.calendarId as string) ?? 'primary',
|
|
393
|
+
eventId: params.eventId as string,
|
|
394
|
+
requestBody,
|
|
395
|
+
});
|
|
396
|
+
return { eventId: res.data.id, status: 'updated' };
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
case 'calendar-delete-event': {
|
|
400
|
+
await client.calendar.events.delete({
|
|
401
|
+
calendarId: (params.calendarId as string) ?? 'primary',
|
|
402
|
+
eventId: params.eventId as string,
|
|
403
|
+
});
|
|
404
|
+
return { eventId: params.eventId, status: 'deleted' };
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
case 'calendar-find-free-slots': {
|
|
408
|
+
const calendarIds = (params.calendarIds as string[] | undefined) ?? ['primary'];
|
|
409
|
+
const res = await client.calendar.freebusy.query({
|
|
410
|
+
requestBody: {
|
|
411
|
+
timeMin: params.timeMin as string,
|
|
412
|
+
timeMax: params.timeMax as string,
|
|
413
|
+
items: calendarIds.map(id => ({ id })),
|
|
414
|
+
},
|
|
415
|
+
});
|
|
416
|
+
const durationMs = ((params.durationMinutes as number) ?? 30) * 60_000;
|
|
417
|
+
const busySlots = Object.values(res.data.calendars ?? {}).flatMap(
|
|
418
|
+
cal => (cal as { busy?: Array<{ start?: string; end?: string }> }).busy ?? [],
|
|
419
|
+
);
|
|
420
|
+
busySlots.sort((a, b) => new Date(a.start!).getTime() - new Date(b.start!).getTime());
|
|
421
|
+
|
|
422
|
+
const windowStart = new Date(params.timeMin as string).getTime();
|
|
423
|
+
const windowEnd = new Date(params.timeMax as string).getTime();
|
|
424
|
+
const slots: Array<{ start: string; end: string }> = [];
|
|
425
|
+
let cursor = windowStart;
|
|
426
|
+
|
|
427
|
+
for (const busy of busySlots) {
|
|
428
|
+
const busyStart = new Date(busy.start!).getTime();
|
|
429
|
+
if (busyStart - cursor >= durationMs) {
|
|
430
|
+
slots.push({ start: new Date(cursor).toISOString(), end: new Date(busyStart).toISOString() });
|
|
431
|
+
}
|
|
432
|
+
cursor = Math.max(cursor, new Date(busy.end!).getTime());
|
|
433
|
+
}
|
|
434
|
+
if (windowEnd - cursor >= durationMs) {
|
|
435
|
+
slots.push({ start: new Date(cursor).toISOString(), end: new Date(windowEnd).toISOString() });
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
return { slots };
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// --- Gmail ---
|
|
442
|
+
case 'gmail-list-messages': {
|
|
443
|
+
const res = await client.gmail.users.messages.list({
|
|
444
|
+
userId: 'me',
|
|
445
|
+
maxResults: (params.maxResults as number) ?? 10,
|
|
446
|
+
q: params.query as string | undefined,
|
|
447
|
+
labelIds: params.labelIds as string[] | undefined,
|
|
448
|
+
});
|
|
449
|
+
return { messages: res.data.messages ?? [] };
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
case 'gmail-read-message': {
|
|
453
|
+
const res = await client.gmail.users.messages.get({
|
|
454
|
+
userId: 'me',
|
|
455
|
+
id: params.messageId as string,
|
|
456
|
+
format: 'full',
|
|
457
|
+
});
|
|
458
|
+
const headers = res.data.payload?.headers ?? [];
|
|
459
|
+
const subject = headers.find(h => h.name === 'Subject')?.value ?? '';
|
|
460
|
+
const from = headers.find(h => h.name === 'From')?.value ?? '';
|
|
461
|
+
const to = headers.find(h => h.name === 'To')?.value ?? '';
|
|
462
|
+
const date = headers.find(h => h.name === 'Date')?.value ?? '';
|
|
463
|
+
const body = extractBody(res.data.payload);
|
|
464
|
+
return { messageId: res.data.id, subject, from, to, date, body, snippet: res.data.snippet };
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
case 'gmail-send': {
|
|
468
|
+
const lines = [
|
|
469
|
+
`To: ${params.to as string}`,
|
|
470
|
+
...(params.cc ? [`Cc: ${params.cc as string}`] : []),
|
|
471
|
+
...(params.bcc ? [`Bcc: ${params.bcc as string}`] : []),
|
|
472
|
+
`Subject: ${params.subject as string}`,
|
|
473
|
+
'Content-Type: text/plain; charset="UTF-8"',
|
|
474
|
+
'',
|
|
475
|
+
params.body as string,
|
|
476
|
+
];
|
|
477
|
+
const raw = Buffer.from(lines.join('\r\n')).toString('base64url');
|
|
478
|
+
const res = await client.gmail.users.messages.send({
|
|
479
|
+
userId: 'me',
|
|
480
|
+
requestBody: { raw },
|
|
481
|
+
});
|
|
482
|
+
return { messageId: res.data.id, status: 'sent' };
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
case 'gmail-draft': {
|
|
486
|
+
const draftLines = [
|
|
487
|
+
`To: ${params.to as string}`,
|
|
488
|
+
`Subject: ${params.subject as string}`,
|
|
489
|
+
'Content-Type: text/plain; charset="UTF-8"',
|
|
490
|
+
'',
|
|
491
|
+
params.body as string,
|
|
492
|
+
];
|
|
493
|
+
const draftRaw = Buffer.from(draftLines.join('\r\n')).toString('base64url');
|
|
494
|
+
const res = await client.gmail.users.drafts.create({
|
|
495
|
+
userId: 'me',
|
|
496
|
+
requestBody: { message: { raw: draftRaw } },
|
|
497
|
+
});
|
|
498
|
+
return { draftId: res.data.id, status: 'created' };
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
case 'gmail-search': {
|
|
502
|
+
const res = await client.gmail.users.messages.list({
|
|
503
|
+
userId: 'me',
|
|
504
|
+
q: params.query as string,
|
|
505
|
+
maxResults: (params.maxResults as number) ?? 10,
|
|
506
|
+
});
|
|
507
|
+
return { messages: res.data.messages ?? [], query: params.query };
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
case 'gmail-archive': {
|
|
511
|
+
await client.gmail.users.messages.modify({
|
|
512
|
+
userId: 'me',
|
|
513
|
+
id: params.messageId as string,
|
|
514
|
+
requestBody: { removeLabelIds: ['INBOX'] },
|
|
515
|
+
});
|
|
516
|
+
return { messageId: params.messageId, status: 'archived' };
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// --- Drive ---
|
|
520
|
+
case 'drive-list-files': {
|
|
521
|
+
const queryParts: string[] = [];
|
|
522
|
+
if (params.folderId) queryParts.push(`'${params.folderId as string}' in parents`);
|
|
523
|
+
if (params.query) queryParts.push(`name contains '${params.query as string}'`);
|
|
524
|
+
const res = await client.drive.files.list({
|
|
525
|
+
pageSize: (params.maxResults as number) ?? 20,
|
|
526
|
+
q: queryParts.length > 0 ? queryParts.join(' and ') : undefined,
|
|
527
|
+
fields: 'files(id,name,mimeType,size,modifiedTime)',
|
|
528
|
+
});
|
|
529
|
+
return { files: res.data.files ?? [] };
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
case 'drive-read-file': {
|
|
533
|
+
const meta = await client.drive.files.get({
|
|
534
|
+
fileId: params.fileId as string,
|
|
535
|
+
fields: 'id,name,mimeType,size',
|
|
536
|
+
});
|
|
537
|
+
const res = await client.drive.files.get(
|
|
538
|
+
{ fileId: params.fileId as string, alt: 'media' },
|
|
539
|
+
{ responseType: 'stream' },
|
|
540
|
+
);
|
|
541
|
+
const chunks: Buffer[] = [];
|
|
542
|
+
for await (const chunk of res.data as Readable) {
|
|
543
|
+
chunks.push(Buffer.from(chunk as Uint8Array));
|
|
544
|
+
}
|
|
545
|
+
const content = Buffer.concat(chunks).toString('utf-8');
|
|
546
|
+
return { fileId: meta.data.id, name: meta.data.name, mimeType: meta.data.mimeType, content };
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
case 'drive-create-file': {
|
|
550
|
+
const fileContent = params.content as string;
|
|
551
|
+
const res = await client.drive.files.create({
|
|
552
|
+
requestBody: {
|
|
553
|
+
name: params.name as string,
|
|
554
|
+
mimeType: (params.mimeType as string) ?? 'text/plain',
|
|
555
|
+
parents: params.folderId ? [params.folderId as string] : undefined,
|
|
556
|
+
},
|
|
557
|
+
media: {
|
|
558
|
+
mimeType: (params.mimeType as string) ?? 'text/plain',
|
|
559
|
+
body: Readable.from([fileContent]),
|
|
560
|
+
},
|
|
561
|
+
fields: 'id,name',
|
|
562
|
+
});
|
|
563
|
+
return { fileId: res.data.id, name: res.data.name, status: 'created' };
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
case 'drive-upload': {
|
|
567
|
+
const uploadBuffer = Buffer.from(params.content as string, 'base64');
|
|
568
|
+
const res = await client.drive.files.create({
|
|
569
|
+
requestBody: {
|
|
570
|
+
name: params.name as string,
|
|
571
|
+
mimeType: params.mimeType as string,
|
|
572
|
+
parents: params.folderId ? [params.folderId as string] : undefined,
|
|
573
|
+
},
|
|
574
|
+
media: {
|
|
575
|
+
mimeType: params.mimeType as string,
|
|
576
|
+
body: Readable.from([uploadBuffer]),
|
|
577
|
+
},
|
|
578
|
+
fields: 'id,name',
|
|
579
|
+
});
|
|
580
|
+
return { fileId: res.data.id, name: res.data.name, status: 'uploaded' };
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
case 'drive-search': {
|
|
584
|
+
const res = await client.drive.files.list({
|
|
585
|
+
q: `fullText contains '${params.query as string}'`,
|
|
586
|
+
pageSize: (params.maxResults as number) ?? 10,
|
|
587
|
+
fields: 'files(id,name,mimeType,size,modifiedTime)',
|
|
588
|
+
});
|
|
589
|
+
return { files: res.data.files ?? [], query: params.query };
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
case 'drive-share': {
|
|
593
|
+
await client.drive.permissions.create({
|
|
594
|
+
fileId: params.fileId as string,
|
|
595
|
+
requestBody: {
|
|
596
|
+
type: 'user',
|
|
597
|
+
role: (params.role as string) ?? 'reader',
|
|
598
|
+
emailAddress: params.email as string,
|
|
599
|
+
},
|
|
600
|
+
});
|
|
601
|
+
return { fileId: params.fileId, sharedWith: params.email, status: 'shared' };
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
default:
|
|
605
|
+
throw new Error(`Unknown action: ${actionId}`);
|
|
606
|
+
}
|
|
607
|
+
},
|
|
608
|
+
|
|
609
|
+
async pollTrigger(triggerId: string, token: string, lastPollAt?: number): Promise<TriggerEvent[]> {
|
|
610
|
+
const client = createGoogleClient(token);
|
|
611
|
+
|
|
612
|
+
switch (triggerId) {
|
|
613
|
+
case 'new-email': {
|
|
614
|
+
const after = Math.floor((lastPollAt ?? Date.now() - 120_000) / 1000);
|
|
615
|
+
const res = await client.gmail.users.messages.list({
|
|
616
|
+
userId: 'me',
|
|
617
|
+
q: `after:${after}`,
|
|
618
|
+
maxResults: 20,
|
|
619
|
+
});
|
|
620
|
+
return (res.data.messages ?? []).map(m => ({
|
|
621
|
+
triggerId: 'new-email',
|
|
622
|
+
connectorId: 'google-workspace',
|
|
623
|
+
timestamp: Date.now(),
|
|
624
|
+
data: { messageId: m.id },
|
|
625
|
+
}));
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
case 'event-starting-soon': {
|
|
629
|
+
const now = new Date();
|
|
630
|
+
const soon = new Date(now.getTime() + 15 * 60_000);
|
|
631
|
+
const res = await client.calendar.events.list({
|
|
632
|
+
calendarId: 'primary',
|
|
633
|
+
timeMin: now.toISOString(),
|
|
634
|
+
timeMax: soon.toISOString(),
|
|
635
|
+
singleEvents: true,
|
|
636
|
+
orderBy: 'startTime',
|
|
637
|
+
});
|
|
638
|
+
return (res.data.items ?? []).map(e => ({
|
|
639
|
+
triggerId: 'event-starting-soon',
|
|
640
|
+
connectorId: 'google-workspace',
|
|
641
|
+
timestamp: Date.now(),
|
|
642
|
+
data: { eventId: e.id, summary: e.summary, start: e.start },
|
|
643
|
+
}));
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
case 'file-shared': {
|
|
647
|
+
const after = lastPollAt
|
|
648
|
+
? new Date(lastPollAt).toISOString()
|
|
649
|
+
: new Date(Date.now() - 300_000).toISOString();
|
|
650
|
+
const res = await client.drive.files.list({
|
|
651
|
+
q: `sharedWithMe = true and modifiedTime > '${after}'`,
|
|
652
|
+
pageSize: 20,
|
|
653
|
+
fields: 'files(id,name,mimeType,modifiedTime,sharingUser)',
|
|
654
|
+
});
|
|
655
|
+
return (res.data.files ?? []).map(f => ({
|
|
656
|
+
triggerId: 'file-shared',
|
|
657
|
+
connectorId: 'google-workspace',
|
|
658
|
+
timestamp: Date.now(),
|
|
659
|
+
data: { fileId: f.id, name: f.name, mimeType: f.mimeType },
|
|
660
|
+
}));
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
default:
|
|
664
|
+
return [];
|
|
665
|
+
}
|
|
666
|
+
},
|
|
667
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { google } from 'googleapis';
|
|
2
|
+
|
|
3
|
+
export function createGoogleClient(accessToken: string) {
|
|
4
|
+
const auth = new google.auth.OAuth2();
|
|
5
|
+
auth.setCredentials({ access_token: accessToken });
|
|
6
|
+
return {
|
|
7
|
+
calendar: google.calendar({ version: 'v3', auth }),
|
|
8
|
+
gmail: google.gmail({ version: 'v1', auth }),
|
|
9
|
+
drive: google.drive({ version: 'v3', auth }),
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export type GoogleClient = ReturnType<typeof createGoogleClient>;
|
package/src/index.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { googleWorkspaceConnector } from './connector.js';
|