n8n-nodes-google-classroom-trigger-student 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/dist/nodes/GoogleClassroomTrigger/GoogleClassroomTrigger.node.d.ts +5 -0
- package/dist/nodes/GoogleClassroomTrigger/GoogleClassroomTrigger.node.js +395 -0
- package/dist/nodes/GoogleClassroomTrigger/GoogleClassroomTrigger.node.js.map +1 -0
- package/index.js +2 -0
- package/nodes/GoogleClassroomTrigger/GoogleClassroomTrigger.node.ts +451 -0
- package/nodes/GoogleClassroomTrigger/googleClassroom.svg +4 -0
- package/package.json +32 -0
- package/tsconfig.json +17 -0
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { INodeExecutionData, INodeType, INodeTypeDescription, IPollFunctions } from 'n8n-workflow';
|
|
2
|
+
export declare class GoogleClassroomTrigger implements INodeType {
|
|
3
|
+
description: INodeTypeDescription;
|
|
4
|
+
poll(this: IPollFunctions): Promise<INodeExecutionData[][] | null>;
|
|
5
|
+
}
|
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.GoogleClassroomTrigger = void 0;
|
|
4
|
+
const n8n_workflow_1 = require("n8n-workflow");
|
|
5
|
+
class GoogleClassroomTrigger {
|
|
6
|
+
constructor() {
|
|
7
|
+
this.description = {
|
|
8
|
+
displayName: 'Google Classroom Trigger',
|
|
9
|
+
name: 'googleClassroomTrigger',
|
|
10
|
+
icon: 'file:googleClassroom.svg',
|
|
11
|
+
group: ['trigger'],
|
|
12
|
+
version: 1,
|
|
13
|
+
description: 'Triggers when a new unsubmitted assignment is posted in Google Classroom',
|
|
14
|
+
subtitle: 'New Assignment',
|
|
15
|
+
polling: true,
|
|
16
|
+
defaults: {
|
|
17
|
+
name: 'Google Classroom Trigger',
|
|
18
|
+
},
|
|
19
|
+
inputs: [],
|
|
20
|
+
outputs: [n8n_workflow_1.NodeConnectionTypes.Main],
|
|
21
|
+
// Reuses n8n built-in Google OAuth2 — identical to Gmail and Drive nodes
|
|
22
|
+
credentials: [
|
|
23
|
+
{
|
|
24
|
+
name: 'googleOAuth2Api',
|
|
25
|
+
required: true,
|
|
26
|
+
},
|
|
27
|
+
],
|
|
28
|
+
properties: [
|
|
29
|
+
{
|
|
30
|
+
displayName: 'Poll Times',
|
|
31
|
+
name: 'pollTimes',
|
|
32
|
+
type: 'fixedCollection',
|
|
33
|
+
typeOptions: {
|
|
34
|
+
multipleValues: true,
|
|
35
|
+
},
|
|
36
|
+
default: { item: [{ mode: 'everyFifteenMinutes' }] },
|
|
37
|
+
description: 'How often to check for new assignments',
|
|
38
|
+
options: [
|
|
39
|
+
{
|
|
40
|
+
name: 'item',
|
|
41
|
+
displayName: 'Item',
|
|
42
|
+
values: [
|
|
43
|
+
{
|
|
44
|
+
displayName: 'Mode',
|
|
45
|
+
name: 'mode',
|
|
46
|
+
type: 'options',
|
|
47
|
+
options: [
|
|
48
|
+
{ name: 'Every Minute', value: 'everyMinute' },
|
|
49
|
+
{ name: 'Every 5 Minutes', value: 'everyFiveMinutes' },
|
|
50
|
+
{ name: 'Every 15 Minutes', value: 'everyFifteenMinutes' },
|
|
51
|
+
{ name: 'Every Hour', value: 'everyHour' },
|
|
52
|
+
{ name: 'Custom (Cron)', value: 'custom' },
|
|
53
|
+
],
|
|
54
|
+
default: 'everyFifteenMinutes',
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
displayName: 'Cron Expression',
|
|
58
|
+
name: 'cronExpression',
|
|
59
|
+
type: 'string',
|
|
60
|
+
displayOptions: {
|
|
61
|
+
show: { mode: ['custom'] },
|
|
62
|
+
},
|
|
63
|
+
default: '0 */15 * * * *',
|
|
64
|
+
description: 'Custom cron expression',
|
|
65
|
+
},
|
|
66
|
+
],
|
|
67
|
+
},
|
|
68
|
+
],
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
displayName: 'This node checks all your enrolled courses and triggers once per new unsubmitted assignment. All attached files are downloaded automatically.',
|
|
72
|
+
name: 'notice',
|
|
73
|
+
type: 'notice',
|
|
74
|
+
default: '',
|
|
75
|
+
},
|
|
76
|
+
],
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
// ─── POLL FUNCTION ────────────────────────────────────────────────
|
|
80
|
+
// Runs on every poll interval — returns new items or null
|
|
81
|
+
async poll() {
|
|
82
|
+
// staticData persists between polls to track seen assignment IDs
|
|
83
|
+
const staticData = this.getWorkflowStaticData('node');
|
|
84
|
+
if (!staticData.lastSeenIds) {
|
|
85
|
+
staticData.lastSeenIds = [];
|
|
86
|
+
}
|
|
87
|
+
const returnData = [];
|
|
88
|
+
try {
|
|
89
|
+
// ── Get OAuth2 access token from n8n Google credential ───────
|
|
90
|
+
const credentials = await this.getCredentials('googleOAuth2Api');
|
|
91
|
+
const tokenData = credentials.oauthTokenData;
|
|
92
|
+
const accessToken = tokenData
|
|
93
|
+
? tokenData.access_token
|
|
94
|
+
: credentials.accessToken;
|
|
95
|
+
const headers = {
|
|
96
|
+
Authorization: `Bearer ${accessToken}`,
|
|
97
|
+
'Content-Type': 'application/json',
|
|
98
|
+
};
|
|
99
|
+
// ── STEP 1: Fetch all active courses student is enrolled in ──
|
|
100
|
+
const coursesResponse = await this.helpers.request({
|
|
101
|
+
method: 'GET',
|
|
102
|
+
url: 'https://classroom.googleapis.com/v1/courses',
|
|
103
|
+
headers,
|
|
104
|
+
qs: {
|
|
105
|
+
studentId: 'me',
|
|
106
|
+
courseStates: 'ACTIVE',
|
|
107
|
+
pageSize: 50,
|
|
108
|
+
},
|
|
109
|
+
json: true,
|
|
110
|
+
});
|
|
111
|
+
const courses = coursesResponse.courses || [];
|
|
112
|
+
// Nothing to do if student has no active courses
|
|
113
|
+
if (courses.length === 0) {
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
// ── STEP 2: Loop through every course ───────────────────────
|
|
117
|
+
for (const course of courses) {
|
|
118
|
+
const courseId = course.id;
|
|
119
|
+
const courseName = course.name;
|
|
120
|
+
// ── STEP 3: Get assignments for this course ──────────────
|
|
121
|
+
let courseWorkItems = [];
|
|
122
|
+
try {
|
|
123
|
+
const courseWorkResponse = await this.helpers.request({
|
|
124
|
+
method: 'GET',
|
|
125
|
+
url: `https://classroom.googleapis.com/v1/courses/${courseId}/courseWork`,
|
|
126
|
+
headers,
|
|
127
|
+
qs: {
|
|
128
|
+
orderBy: 'updateTime desc',
|
|
129
|
+
pageSize: 20,
|
|
130
|
+
},
|
|
131
|
+
json: true,
|
|
132
|
+
});
|
|
133
|
+
courseWorkItems =
|
|
134
|
+
courseWorkResponse.courseWork || [];
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
// Skip this course if student has no access
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
// ── STEP 4: Process each assignment ─────────────────────
|
|
141
|
+
for (const work of courseWorkItems) {
|
|
142
|
+
const workId = work.id;
|
|
143
|
+
const workTitle = work.title;
|
|
144
|
+
// Skip assignments we already processed in a previous poll
|
|
145
|
+
if (staticData.lastSeenIds.includes(workId)) {
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
// ── STEP 5: Check if student already submitted ─────────
|
|
149
|
+
let submissionState = 'NEW';
|
|
150
|
+
try {
|
|
151
|
+
const subResponse = await this.helpers.request({
|
|
152
|
+
method: 'GET',
|
|
153
|
+
url: `https://classroom.googleapis.com/v1/courses/${courseId}/courseWork/${workId}/studentSubmissions`,
|
|
154
|
+
headers,
|
|
155
|
+
qs: { userId: 'me' },
|
|
156
|
+
json: true,
|
|
157
|
+
});
|
|
158
|
+
const submissions = subResponse.studentSubmissions || [];
|
|
159
|
+
if (submissions.length > 0) {
|
|
160
|
+
submissionState = submissions[0].state;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
catch {
|
|
164
|
+
// If check fails, treat as not submitted so we don't miss it
|
|
165
|
+
submissionState = 'NEW';
|
|
166
|
+
}
|
|
167
|
+
// ── STEP 6: Skip already submitted assignments ──────────
|
|
168
|
+
// States: NEW, CREATED, TURNED_IN, RETURNED, RECLAIMED_BY_STUDENT
|
|
169
|
+
if (submissionState === 'TURNED_IN' ||
|
|
170
|
+
submissionState === 'RETURNED') {
|
|
171
|
+
staticData.lastSeenIds.push(workId);
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
// ── STEP 7: Format deadline as human readable ───────────
|
|
175
|
+
let deadlineFormatted = 'No deadline set';
|
|
176
|
+
if (work.dueDate) {
|
|
177
|
+
const due = work.dueDate;
|
|
178
|
+
const dueTime = work.dueTime || {};
|
|
179
|
+
const year = due.year;
|
|
180
|
+
const month = due.month; // 1–12
|
|
181
|
+
const day = due.day;
|
|
182
|
+
const hours = dueTime.hours || 23;
|
|
183
|
+
const minutes = dueTime.minutes || 59;
|
|
184
|
+
const dateObj = new Date(year, month - 1, day, hours, minutes);
|
|
185
|
+
// Output: "March 25, 2025 at 11:59 PM"
|
|
186
|
+
deadlineFormatted = dateObj.toLocaleString('en-US', {
|
|
187
|
+
year: 'numeric',
|
|
188
|
+
month: 'long',
|
|
189
|
+
day: 'numeric',
|
|
190
|
+
hour: 'numeric',
|
|
191
|
+
minute: '2-digit',
|
|
192
|
+
hour12: true,
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
// ── STEP 8: Process all attachments ─────────────────────
|
|
196
|
+
const materials = work.materials || [];
|
|
197
|
+
if (materials.length === 0) {
|
|
198
|
+
// No attachments — emit metadata only item
|
|
199
|
+
returnData.push({
|
|
200
|
+
json: {
|
|
201
|
+
courseId,
|
|
202
|
+
courseName,
|
|
203
|
+
assignmentId: workId,
|
|
204
|
+
assignmentTitle: workTitle,
|
|
205
|
+
description: work.description || '',
|
|
206
|
+
deadline: deadlineFormatted,
|
|
207
|
+
totalPoints: work.maxPoints || 0,
|
|
208
|
+
submissionState,
|
|
209
|
+
hasAttachments: false,
|
|
210
|
+
fileName: null,
|
|
211
|
+
fileMimeType: null,
|
|
212
|
+
fileType: null,
|
|
213
|
+
classroomLink: work.alternateLink || '',
|
|
214
|
+
},
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
else {
|
|
218
|
+
// One output item per attachment file
|
|
219
|
+
for (const material of materials) {
|
|
220
|
+
// ── YouTube video — no binary, pass URL only ─────────
|
|
221
|
+
if (material.youtubeVideo) {
|
|
222
|
+
const yt = material.youtubeVideo;
|
|
223
|
+
returnData.push({
|
|
224
|
+
json: {
|
|
225
|
+
courseId,
|
|
226
|
+
courseName,
|
|
227
|
+
assignmentId: workId,
|
|
228
|
+
assignmentTitle: workTitle,
|
|
229
|
+
description: work.description || '',
|
|
230
|
+
deadline: deadlineFormatted,
|
|
231
|
+
totalPoints: work.maxPoints || 0,
|
|
232
|
+
submissionState,
|
|
233
|
+
hasAttachments: true,
|
|
234
|
+
fileName: yt.title || 'YouTube Video',
|
|
235
|
+
fileMimeType: 'video/youtube',
|
|
236
|
+
fileType: 'youtubeVideo',
|
|
237
|
+
youtubeUrl: yt.alternateLink,
|
|
238
|
+
classroomLink: work.alternateLink || '',
|
|
239
|
+
},
|
|
240
|
+
});
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
// ── External link — no binary, pass URL only ─────────
|
|
244
|
+
if (material.link) {
|
|
245
|
+
const lk = material.link;
|
|
246
|
+
returnData.push({
|
|
247
|
+
json: {
|
|
248
|
+
courseId,
|
|
249
|
+
courseName,
|
|
250
|
+
assignmentId: workId,
|
|
251
|
+
assignmentTitle: workTitle,
|
|
252
|
+
description: work.description || '',
|
|
253
|
+
deadline: deadlineFormatted,
|
|
254
|
+
totalPoints: work.maxPoints || 0,
|
|
255
|
+
submissionState,
|
|
256
|
+
hasAttachments: true,
|
|
257
|
+
fileName: lk.title || lk.url,
|
|
258
|
+
fileMimeType: 'text/html',
|
|
259
|
+
fileType: 'link',
|
|
260
|
+
linkUrl: lk.url,
|
|
261
|
+
classroomLink: work.alternateLink || '',
|
|
262
|
+
},
|
|
263
|
+
});
|
|
264
|
+
continue;
|
|
265
|
+
}
|
|
266
|
+
// ── Google Form — no binary, pass URL only ───────────
|
|
267
|
+
if (material.form) {
|
|
268
|
+
const fm = material.form;
|
|
269
|
+
returnData.push({
|
|
270
|
+
json: {
|
|
271
|
+
courseId,
|
|
272
|
+
courseName,
|
|
273
|
+
assignmentId: workId,
|
|
274
|
+
assignmentTitle: workTitle,
|
|
275
|
+
description: work.description || '',
|
|
276
|
+
deadline: deadlineFormatted,
|
|
277
|
+
totalPoints: work.maxPoints || 0,
|
|
278
|
+
submissionState,
|
|
279
|
+
hasAttachments: true,
|
|
280
|
+
fileName: fm.title || 'Google Form',
|
|
281
|
+
fileMimeType: 'application/form',
|
|
282
|
+
fileType: 'form',
|
|
283
|
+
formUrl: fm.formUrl,
|
|
284
|
+
classroomLink: work.alternateLink || '',
|
|
285
|
+
},
|
|
286
|
+
});
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
// ── Drive file — download as binary ──────────────────
|
|
290
|
+
if (material.driveFile) {
|
|
291
|
+
const df = material.driveFile;
|
|
292
|
+
const f = df.driveFile;
|
|
293
|
+
const driveFileId = f.id;
|
|
294
|
+
let fileName = f.title || 'file';
|
|
295
|
+
let fileMimeType = f.mimeType || 'application/octet-stream';
|
|
296
|
+
let downloadUrl = '';
|
|
297
|
+
// Google Workspace files must be exported
|
|
298
|
+
// Binary files (PDF, images, zip) use alt=media
|
|
299
|
+
if (fileMimeType.startsWith('application/vnd.google-apps')) {
|
|
300
|
+
const exportMap = {
|
|
301
|
+
'application/vnd.google-apps.document': 'application/pdf',
|
|
302
|
+
'application/vnd.google-apps.spreadsheet': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
303
|
+
'application/vnd.google-apps.presentation': 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
|
304
|
+
'application/vnd.google-apps.form': 'application/pdf',
|
|
305
|
+
};
|
|
306
|
+
const exportMime = exportMap[fileMimeType] || 'application/pdf';
|
|
307
|
+
downloadUrl =
|
|
308
|
+
`https://www.googleapis.com/drive/v3/files/${driveFileId}/export?mimeType=${encodeURIComponent(exportMime)}`;
|
|
309
|
+
fileMimeType = exportMime;
|
|
310
|
+
}
|
|
311
|
+
else {
|
|
312
|
+
// Direct binary download for PDF, images, zip, etc.
|
|
313
|
+
downloadUrl =
|
|
314
|
+
`https://www.googleapis.com/drive/v3/files/${driveFileId}?alt=media`;
|
|
315
|
+
}
|
|
316
|
+
try {
|
|
317
|
+
// Download file as raw buffer
|
|
318
|
+
const fileBuffer = await this.helpers.request({
|
|
319
|
+
method: 'GET',
|
|
320
|
+
url: downloadUrl,
|
|
321
|
+
headers,
|
|
322
|
+
encoding: null,
|
|
323
|
+
});
|
|
324
|
+
// Convert buffer to n8n binary format
|
|
325
|
+
const binaryData = await this.helpers.prepareBinaryData(fileBuffer, fileName, fileMimeType);
|
|
326
|
+
returnData.push({
|
|
327
|
+
json: {
|
|
328
|
+
courseId,
|
|
329
|
+
courseName,
|
|
330
|
+
assignmentId: workId,
|
|
331
|
+
assignmentTitle: workTitle,
|
|
332
|
+
description: work.description || '',
|
|
333
|
+
deadline: deadlineFormatted,
|
|
334
|
+
totalPoints: work.maxPoints || 0,
|
|
335
|
+
submissionState,
|
|
336
|
+
hasAttachments: true,
|
|
337
|
+
fileName,
|
|
338
|
+
fileMimeType,
|
|
339
|
+
fileType: 'driveFile',
|
|
340
|
+
driveFileId,
|
|
341
|
+
classroomLink: work.alternateLink || '',
|
|
342
|
+
},
|
|
343
|
+
binary: {
|
|
344
|
+
data: binaryData,
|
|
345
|
+
},
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
catch {
|
|
349
|
+
// Download failed — still emit JSON so workflow
|
|
350
|
+
// does not break silently
|
|
351
|
+
returnData.push({
|
|
352
|
+
json: {
|
|
353
|
+
courseId,
|
|
354
|
+
courseName,
|
|
355
|
+
assignmentId: workId,
|
|
356
|
+
assignmentTitle: workTitle,
|
|
357
|
+
description: work.description || '',
|
|
358
|
+
deadline: deadlineFormatted,
|
|
359
|
+
totalPoints: work.maxPoints || 0,
|
|
360
|
+
submissionState,
|
|
361
|
+
hasAttachments: true,
|
|
362
|
+
fileName,
|
|
363
|
+
fileMimeType,
|
|
364
|
+
fileType: 'driveFile',
|
|
365
|
+
driveFileId,
|
|
366
|
+
classroomLink: work.alternateLink || '',
|
|
367
|
+
downloadError: 'File download failed — check Drive permissions',
|
|
368
|
+
},
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
// Mark this assignment as seen to prevent duplicate triggers
|
|
375
|
+
staticData.lastSeenIds.push(workId);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
// Trim seen IDs to last 500 to prevent memory growing forever
|
|
379
|
+
if (staticData.lastSeenIds.length > 500) {
|
|
380
|
+
staticData.lastSeenIds = staticData.lastSeenIds.slice(-500);
|
|
381
|
+
}
|
|
382
|
+
// Return null if nothing new found this poll cycle
|
|
383
|
+
return returnData.length === 0 ? null : [returnData];
|
|
384
|
+
}
|
|
385
|
+
catch (error) {
|
|
386
|
+
// Throw plain Error — works cleanly with all n8n TypeScript versions
|
|
387
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
388
|
+
throw new Error(`Google Classroom Trigger failed: ${errorMessage}. ` +
|
|
389
|
+
`Please check your Google OAuth2 credentials and ensure ` +
|
|
390
|
+
`Classroom API and Drive API are enabled in Google Cloud Console.`);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
exports.GoogleClassroomTrigger = GoogleClassroomTrigger;
|
|
395
|
+
//# sourceMappingURL=GoogleClassroomTrigger.node.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"GoogleClassroomTrigger.node.js","sourceRoot":"","sources":["../../../nodes/GoogleClassroomTrigger/GoogleClassroomTrigger.node.ts"],"names":[],"mappings":"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"}
|
package/index.js
ADDED
|
@@ -0,0 +1,451 @@
|
|
|
1
|
+
import {
|
|
2
|
+
IDataObject,
|
|
3
|
+
INodeExecutionData,
|
|
4
|
+
INodeType,
|
|
5
|
+
INodeTypeDescription,
|
|
6
|
+
IPollFunctions,
|
|
7
|
+
NodeConnectionTypes,
|
|
8
|
+
} from 'n8n-workflow';
|
|
9
|
+
|
|
10
|
+
export class GoogleClassroomTrigger implements INodeType {
|
|
11
|
+
description: INodeTypeDescription = {
|
|
12
|
+
displayName: 'Google Classroom Trigger',
|
|
13
|
+
name: 'googleClassroomTrigger',
|
|
14
|
+
icon: 'file:googleClassroom.svg',
|
|
15
|
+
group: ['trigger'],
|
|
16
|
+
version: 1,
|
|
17
|
+
description: 'Triggers when a new unsubmitted assignment is posted in Google Classroom',
|
|
18
|
+
subtitle: 'New Assignment',
|
|
19
|
+
polling: true,
|
|
20
|
+
defaults: {
|
|
21
|
+
name: 'Google Classroom Trigger',
|
|
22
|
+
},
|
|
23
|
+
inputs: [],
|
|
24
|
+
outputs: [NodeConnectionTypes.Main],
|
|
25
|
+
|
|
26
|
+
// Reuses n8n built-in Google OAuth2 — identical to Gmail and Drive nodes
|
|
27
|
+
credentials: [
|
|
28
|
+
{
|
|
29
|
+
name: 'googleOAuth2Api',
|
|
30
|
+
required: true,
|
|
31
|
+
},
|
|
32
|
+
],
|
|
33
|
+
|
|
34
|
+
properties: [
|
|
35
|
+
{
|
|
36
|
+
displayName: 'Poll Times',
|
|
37
|
+
name: 'pollTimes',
|
|
38
|
+
type: 'fixedCollection',
|
|
39
|
+
typeOptions: {
|
|
40
|
+
multipleValues: true,
|
|
41
|
+
},
|
|
42
|
+
default: { item: [{ mode: 'everyFifteenMinutes' }] },
|
|
43
|
+
description: 'How often to check for new assignments',
|
|
44
|
+
options: [
|
|
45
|
+
{
|
|
46
|
+
name: 'item',
|
|
47
|
+
displayName: 'Item',
|
|
48
|
+
values: [
|
|
49
|
+
{
|
|
50
|
+
displayName: 'Mode',
|
|
51
|
+
name: 'mode',
|
|
52
|
+
type: 'options',
|
|
53
|
+
options: [
|
|
54
|
+
{ name: 'Every Minute', value: 'everyMinute' },
|
|
55
|
+
{ name: 'Every 5 Minutes', value: 'everyFiveMinutes' },
|
|
56
|
+
{ name: 'Every 15 Minutes', value: 'everyFifteenMinutes' },
|
|
57
|
+
{ name: 'Every Hour', value: 'everyHour' },
|
|
58
|
+
{ name: 'Custom (Cron)', value: 'custom' },
|
|
59
|
+
],
|
|
60
|
+
default: 'everyFifteenMinutes',
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
displayName: 'Cron Expression',
|
|
64
|
+
name: 'cronExpression',
|
|
65
|
+
type: 'string',
|
|
66
|
+
displayOptions: {
|
|
67
|
+
show: { mode: ['custom'] },
|
|
68
|
+
},
|
|
69
|
+
default: '0 */15 * * * *',
|
|
70
|
+
description: 'Custom cron expression',
|
|
71
|
+
},
|
|
72
|
+
],
|
|
73
|
+
},
|
|
74
|
+
],
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
displayName: 'This node checks all your enrolled courses and triggers once per new unsubmitted assignment. All attached files are downloaded automatically.',
|
|
78
|
+
name: 'notice',
|
|
79
|
+
type: 'notice',
|
|
80
|
+
default: '',
|
|
81
|
+
},
|
|
82
|
+
],
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
// ─── POLL FUNCTION ────────────────────────────────────────────────
|
|
86
|
+
// Runs on every poll interval — returns new items or null
|
|
87
|
+
async poll(this: IPollFunctions): Promise<INodeExecutionData[][] | null> {
|
|
88
|
+
|
|
89
|
+
// staticData persists between polls to track seen assignment IDs
|
|
90
|
+
const staticData = this.getWorkflowStaticData('node') as {
|
|
91
|
+
lastSeenIds?: string[];
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
if (!staticData.lastSeenIds) {
|
|
95
|
+
staticData.lastSeenIds = [];
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const returnData: INodeExecutionData[] = [];
|
|
99
|
+
|
|
100
|
+
try {
|
|
101
|
+
// ── Get OAuth2 access token from n8n Google credential ───────
|
|
102
|
+
const credentials = await this.getCredentials('googleOAuth2Api');
|
|
103
|
+
const tokenData = credentials.oauthTokenData as IDataObject | undefined;
|
|
104
|
+
const accessToken = tokenData
|
|
105
|
+
? (tokenData.access_token as string)
|
|
106
|
+
: (credentials.accessToken as string);
|
|
107
|
+
|
|
108
|
+
const headers = {
|
|
109
|
+
Authorization: `Bearer ${accessToken}`,
|
|
110
|
+
'Content-Type': 'application/json',
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
// ── STEP 1: Fetch all active courses student is enrolled in ──
|
|
114
|
+
const coursesResponse = await this.helpers.request({
|
|
115
|
+
method: 'GET',
|
|
116
|
+
url: 'https://classroom.googleapis.com/v1/courses',
|
|
117
|
+
headers,
|
|
118
|
+
qs: {
|
|
119
|
+
studentId: 'me',
|
|
120
|
+
courseStates: 'ACTIVE',
|
|
121
|
+
pageSize: 50,
|
|
122
|
+
},
|
|
123
|
+
json: true,
|
|
124
|
+
}) as IDataObject;
|
|
125
|
+
|
|
126
|
+
const courses: IDataObject[] =
|
|
127
|
+
(coursesResponse.courses as IDataObject[]) || [];
|
|
128
|
+
|
|
129
|
+
// Nothing to do if student has no active courses
|
|
130
|
+
if (courses.length === 0) {
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// ── STEP 2: Loop through every course ───────────────────────
|
|
135
|
+
for (const course of courses) {
|
|
136
|
+
const courseId = course.id as string;
|
|
137
|
+
const courseName = course.name as string;
|
|
138
|
+
|
|
139
|
+
// ── STEP 3: Get assignments for this course ──────────────
|
|
140
|
+
let courseWorkItems: IDataObject[] = [];
|
|
141
|
+
try {
|
|
142
|
+
const courseWorkResponse = await this.helpers.request({
|
|
143
|
+
method: 'GET',
|
|
144
|
+
url: `https://classroom.googleapis.com/v1/courses/${courseId}/courseWork`,
|
|
145
|
+
headers,
|
|
146
|
+
qs: {
|
|
147
|
+
orderBy: 'updateTime desc',
|
|
148
|
+
pageSize: 20,
|
|
149
|
+
},
|
|
150
|
+
json: true,
|
|
151
|
+
}) as IDataObject;
|
|
152
|
+
|
|
153
|
+
courseWorkItems =
|
|
154
|
+
(courseWorkResponse.courseWork as IDataObject[]) || [];
|
|
155
|
+
} catch {
|
|
156
|
+
// Skip this course if student has no access
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// ── STEP 4: Process each assignment ─────────────────────
|
|
161
|
+
for (const work of courseWorkItems) {
|
|
162
|
+
const workId = work.id as string;
|
|
163
|
+
const workTitle = work.title as string;
|
|
164
|
+
|
|
165
|
+
// Skip assignments we already processed in a previous poll
|
|
166
|
+
if (staticData.lastSeenIds!.includes(workId)) {
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// ── STEP 5: Check if student already submitted ─────────
|
|
171
|
+
let submissionState = 'NEW';
|
|
172
|
+
try {
|
|
173
|
+
const subResponse = await this.helpers.request({
|
|
174
|
+
method: 'GET',
|
|
175
|
+
url: `https://classroom.googleapis.com/v1/courses/${courseId}/courseWork/${workId}/studentSubmissions`,
|
|
176
|
+
headers,
|
|
177
|
+
qs: { userId: 'me' },
|
|
178
|
+
json: true,
|
|
179
|
+
}) as IDataObject;
|
|
180
|
+
|
|
181
|
+
const submissions =
|
|
182
|
+
(subResponse.studentSubmissions as IDataObject[]) || [];
|
|
183
|
+
|
|
184
|
+
if (submissions.length > 0) {
|
|
185
|
+
submissionState = submissions[0].state as string;
|
|
186
|
+
}
|
|
187
|
+
} catch {
|
|
188
|
+
// If check fails, treat as not submitted so we don't miss it
|
|
189
|
+
submissionState = 'NEW';
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// ── STEP 6: Skip already submitted assignments ──────────
|
|
193
|
+
// States: NEW, CREATED, TURNED_IN, RETURNED, RECLAIMED_BY_STUDENT
|
|
194
|
+
if (
|
|
195
|
+
submissionState === 'TURNED_IN' ||
|
|
196
|
+
submissionState === 'RETURNED'
|
|
197
|
+
) {
|
|
198
|
+
staticData.lastSeenIds!.push(workId);
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// ── STEP 7: Format deadline as human readable ───────────
|
|
203
|
+
let deadlineFormatted = 'No deadline set';
|
|
204
|
+
|
|
205
|
+
if (work.dueDate) {
|
|
206
|
+
const due = work.dueDate as IDataObject;
|
|
207
|
+
const dueTime = (work.dueTime as IDataObject) || {};
|
|
208
|
+
|
|
209
|
+
const year = due.year as number;
|
|
210
|
+
const month = due.month as number; // 1–12
|
|
211
|
+
const day = due.day as number;
|
|
212
|
+
const hours = (dueTime.hours as number) || 23;
|
|
213
|
+
const minutes = (dueTime.minutes as number) || 59;
|
|
214
|
+
|
|
215
|
+
const dateObj = new Date(year, month - 1, day, hours, minutes);
|
|
216
|
+
|
|
217
|
+
// Output: "March 25, 2025 at 11:59 PM"
|
|
218
|
+
deadlineFormatted = dateObj.toLocaleString('en-US', {
|
|
219
|
+
year: 'numeric',
|
|
220
|
+
month: 'long',
|
|
221
|
+
day: 'numeric',
|
|
222
|
+
hour: 'numeric',
|
|
223
|
+
minute: '2-digit',
|
|
224
|
+
hour12: true,
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// ── STEP 8: Process all attachments ─────────────────────
|
|
229
|
+
const materials = (work.materials as IDataObject[]) || [];
|
|
230
|
+
|
|
231
|
+
if (materials.length === 0) {
|
|
232
|
+
// No attachments — emit metadata only item
|
|
233
|
+
returnData.push({
|
|
234
|
+
json: {
|
|
235
|
+
courseId,
|
|
236
|
+
courseName,
|
|
237
|
+
assignmentId: workId,
|
|
238
|
+
assignmentTitle: workTitle,
|
|
239
|
+
description: (work.description as string) || '',
|
|
240
|
+
deadline: deadlineFormatted,
|
|
241
|
+
totalPoints: (work.maxPoints as number) || 0,
|
|
242
|
+
submissionState,
|
|
243
|
+
hasAttachments: false,
|
|
244
|
+
fileName: null,
|
|
245
|
+
fileMimeType: null,
|
|
246
|
+
fileType: null,
|
|
247
|
+
classroomLink: (work.alternateLink as string) || '',
|
|
248
|
+
},
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
} else {
|
|
252
|
+
|
|
253
|
+
// One output item per attachment file
|
|
254
|
+
for (const material of materials) {
|
|
255
|
+
|
|
256
|
+
// ── YouTube video — no binary, pass URL only ─────────
|
|
257
|
+
if (material.youtubeVideo) {
|
|
258
|
+
const yt = material.youtubeVideo as IDataObject;
|
|
259
|
+
returnData.push({
|
|
260
|
+
json: {
|
|
261
|
+
courseId,
|
|
262
|
+
courseName,
|
|
263
|
+
assignmentId: workId,
|
|
264
|
+
assignmentTitle: workTitle,
|
|
265
|
+
description: (work.description as string) || '',
|
|
266
|
+
deadline: deadlineFormatted,
|
|
267
|
+
totalPoints: (work.maxPoints as number) || 0,
|
|
268
|
+
submissionState,
|
|
269
|
+
hasAttachments: true,
|
|
270
|
+
fileName: (yt.title as string) || 'YouTube Video',
|
|
271
|
+
fileMimeType: 'video/youtube',
|
|
272
|
+
fileType: 'youtubeVideo',
|
|
273
|
+
youtubeUrl: yt.alternateLink as string,
|
|
274
|
+
classroomLink: (work.alternateLink as string) || '',
|
|
275
|
+
},
|
|
276
|
+
});
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// ── External link — no binary, pass URL only ─────────
|
|
281
|
+
if (material.link) {
|
|
282
|
+
const lk = material.link as IDataObject;
|
|
283
|
+
returnData.push({
|
|
284
|
+
json: {
|
|
285
|
+
courseId,
|
|
286
|
+
courseName,
|
|
287
|
+
assignmentId: workId,
|
|
288
|
+
assignmentTitle: workTitle,
|
|
289
|
+
description: (work.description as string) || '',
|
|
290
|
+
deadline: deadlineFormatted,
|
|
291
|
+
totalPoints: (work.maxPoints as number) || 0,
|
|
292
|
+
submissionState,
|
|
293
|
+
hasAttachments: true,
|
|
294
|
+
fileName: (lk.title as string) || (lk.url as string),
|
|
295
|
+
fileMimeType: 'text/html',
|
|
296
|
+
fileType: 'link',
|
|
297
|
+
linkUrl: lk.url as string,
|
|
298
|
+
classroomLink: (work.alternateLink as string) || '',
|
|
299
|
+
},
|
|
300
|
+
});
|
|
301
|
+
continue;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// ── Google Form — no binary, pass URL only ───────────
|
|
305
|
+
if (material.form) {
|
|
306
|
+
const fm = material.form as IDataObject;
|
|
307
|
+
returnData.push({
|
|
308
|
+
json: {
|
|
309
|
+
courseId,
|
|
310
|
+
courseName,
|
|
311
|
+
assignmentId: workId,
|
|
312
|
+
assignmentTitle: workTitle,
|
|
313
|
+
description: (work.description as string) || '',
|
|
314
|
+
deadline: deadlineFormatted,
|
|
315
|
+
totalPoints: (work.maxPoints as number) || 0,
|
|
316
|
+
submissionState,
|
|
317
|
+
hasAttachments: true,
|
|
318
|
+
fileName: (fm.title as string) || 'Google Form',
|
|
319
|
+
fileMimeType: 'application/form',
|
|
320
|
+
fileType: 'form',
|
|
321
|
+
formUrl: fm.formUrl as string,
|
|
322
|
+
classroomLink: (work.alternateLink as string) || '',
|
|
323
|
+
},
|
|
324
|
+
});
|
|
325
|
+
continue;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// ── Drive file — download as binary ──────────────────
|
|
329
|
+
if (material.driveFile) {
|
|
330
|
+
const df = material.driveFile as IDataObject;
|
|
331
|
+
const f = df.driveFile as IDataObject;
|
|
332
|
+
const driveFileId = f.id as string;
|
|
333
|
+
let fileName = (f.title as string) || 'file';
|
|
334
|
+
let fileMimeType = (f.mimeType as string) || 'application/octet-stream';
|
|
335
|
+
let downloadUrl = '';
|
|
336
|
+
|
|
337
|
+
// Google Workspace files must be exported
|
|
338
|
+
// Binary files (PDF, images, zip) use alt=media
|
|
339
|
+
if (fileMimeType.startsWith('application/vnd.google-apps')) {
|
|
340
|
+
const exportMap: Record<string, string> = {
|
|
341
|
+
'application/vnd.google-apps.document':
|
|
342
|
+
'application/pdf',
|
|
343
|
+
'application/vnd.google-apps.spreadsheet':
|
|
344
|
+
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
345
|
+
'application/vnd.google-apps.presentation':
|
|
346
|
+
'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
|
347
|
+
'application/vnd.google-apps.form':
|
|
348
|
+
'application/pdf',
|
|
349
|
+
};
|
|
350
|
+
const exportMime =
|
|
351
|
+
exportMap[fileMimeType] || 'application/pdf';
|
|
352
|
+
downloadUrl =
|
|
353
|
+
`https://www.googleapis.com/drive/v3/files/${driveFileId}/export?mimeType=${encodeURIComponent(exportMime)}`;
|
|
354
|
+
fileMimeType = exportMime;
|
|
355
|
+
} else {
|
|
356
|
+
// Direct binary download for PDF, images, zip, etc.
|
|
357
|
+
downloadUrl =
|
|
358
|
+
`https://www.googleapis.com/drive/v3/files/${driveFileId}?alt=media`;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
try {
|
|
362
|
+
// Download file as raw buffer
|
|
363
|
+
const fileBuffer = await this.helpers.request({
|
|
364
|
+
method: 'GET',
|
|
365
|
+
url: downloadUrl,
|
|
366
|
+
headers,
|
|
367
|
+
encoding: null,
|
|
368
|
+
}) as Buffer;
|
|
369
|
+
|
|
370
|
+
// Convert buffer to n8n binary format
|
|
371
|
+
const binaryData = await this.helpers.prepareBinaryData(
|
|
372
|
+
fileBuffer,
|
|
373
|
+
fileName,
|
|
374
|
+
fileMimeType,
|
|
375
|
+
);
|
|
376
|
+
|
|
377
|
+
returnData.push({
|
|
378
|
+
json: {
|
|
379
|
+
courseId,
|
|
380
|
+
courseName,
|
|
381
|
+
assignmentId: workId,
|
|
382
|
+
assignmentTitle: workTitle,
|
|
383
|
+
description: (work.description as string) || '',
|
|
384
|
+
deadline: deadlineFormatted,
|
|
385
|
+
totalPoints: (work.maxPoints as number) || 0,
|
|
386
|
+
submissionState,
|
|
387
|
+
hasAttachments: true,
|
|
388
|
+
fileName,
|
|
389
|
+
fileMimeType,
|
|
390
|
+
fileType: 'driveFile',
|
|
391
|
+
driveFileId,
|
|
392
|
+
classroomLink: (work.alternateLink as string) || '',
|
|
393
|
+
},
|
|
394
|
+
binary: {
|
|
395
|
+
data: binaryData,
|
|
396
|
+
},
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
} catch {
|
|
400
|
+
// Download failed — still emit JSON so workflow
|
|
401
|
+
// does not break silently
|
|
402
|
+
returnData.push({
|
|
403
|
+
json: {
|
|
404
|
+
courseId,
|
|
405
|
+
courseName,
|
|
406
|
+
assignmentId: workId,
|
|
407
|
+
assignmentTitle: workTitle,
|
|
408
|
+
description: (work.description as string) || '',
|
|
409
|
+
deadline: deadlineFormatted,
|
|
410
|
+
totalPoints: (work.maxPoints as number) || 0,
|
|
411
|
+
submissionState,
|
|
412
|
+
hasAttachments: true,
|
|
413
|
+
fileName,
|
|
414
|
+
fileMimeType,
|
|
415
|
+
fileType: 'driveFile',
|
|
416
|
+
driveFileId,
|
|
417
|
+
classroomLink: (work.alternateLink as string) || '',
|
|
418
|
+
downloadError:
|
|
419
|
+
'File download failed — check Drive permissions',
|
|
420
|
+
},
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// Mark this assignment as seen to prevent duplicate triggers
|
|
428
|
+
staticData.lastSeenIds!.push(workId);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// Trim seen IDs to last 500 to prevent memory growing forever
|
|
433
|
+
if (staticData.lastSeenIds!.length > 500) {
|
|
434
|
+
staticData.lastSeenIds = staticData.lastSeenIds!.slice(-500);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// Return null if nothing new found this poll cycle
|
|
438
|
+
return returnData.length === 0 ? null : [returnData];
|
|
439
|
+
|
|
440
|
+
} catch (error) {
|
|
441
|
+
// Throw plain Error — works cleanly with all n8n TypeScript versions
|
|
442
|
+
const errorMessage =
|
|
443
|
+
error instanceof Error ? error.message : 'Unknown error occurred';
|
|
444
|
+
throw new Error(
|
|
445
|
+
`Google Classroom Trigger failed: ${errorMessage}. ` +
|
|
446
|
+
`Please check your Google OAuth2 credentials and ensure ` +
|
|
447
|
+
`Classroom API and Drive API are enabled in Google Cloud Console.`
|
|
448
|
+
);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "n8n-nodes-google-classroom-trigger-student",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Google Classroom Trigger for n8n - triggers on new unsubmitted assignments and downloads all attachments",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"n8n-community-node-package",
|
|
7
|
+
"n8n",
|
|
8
|
+
"google-classroom",
|
|
9
|
+
"classroom",
|
|
10
|
+
"education",
|
|
11
|
+
"trigger",
|
|
12
|
+
"student"
|
|
13
|
+
],
|
|
14
|
+
"license": "MIT",
|
|
15
|
+
"main": "index.js",
|
|
16
|
+
"scripts": {
|
|
17
|
+
"build": "tsc"
|
|
18
|
+
},
|
|
19
|
+
"n8n": {
|
|
20
|
+
"n8nNodesApiVersion": 1,
|
|
21
|
+
"nodes": [
|
|
22
|
+
"dist/nodes/GoogleClassroomTrigger/GoogleClassroomTrigger.node.js"
|
|
23
|
+
]
|
|
24
|
+
},
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"typescript": "^5.0.0",
|
|
27
|
+
"@types/node": "^18.0.0"
|
|
28
|
+
},
|
|
29
|
+
"peerDependencies": {
|
|
30
|
+
"n8n-workflow": "*"
|
|
31
|
+
}
|
|
32
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2019",
|
|
4
|
+
"module": "commonjs",
|
|
5
|
+
"lib": ["ES2019"],
|
|
6
|
+
"outDir": "./dist",
|
|
7
|
+
"rootDir": "./",
|
|
8
|
+
"strict": false,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"resolveJsonModule": true,
|
|
11
|
+
"declaration": true,
|
|
12
|
+
"sourceMap": true,
|
|
13
|
+
"skipLibCheck": true
|
|
14
|
+
},
|
|
15
|
+
"include": ["nodes/**/*"],
|
|
16
|
+
"exclude": ["node_modules", "dist"]
|
|
17
|
+
}
|