n8n-nodes-pronote 0.1.2
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/README.md +24 -0
- package/dist/agent-tools/pronoteTools.d.ts +8 -0
- package/dist/agent-tools/pronoteTools.js +23 -0
- package/dist/credentials/PronoteApi.credentials.d.ts +7 -0
- package/dist/credentials/PronoteApi.credentials.js +101 -0
- package/dist/nodes/Pronote/GenericFunctions.d.ts +6 -0
- package/dist/nodes/Pronote/GenericFunctions.js +30 -0
- package/dist/nodes/Pronote/Pronote.node.d.ts +5 -0
- package/dist/nodes/Pronote/Pronote.node.js +265 -0
- package/dist/shared/pawnoteClient.d.ts +109 -0
- package/dist/shared/pawnoteClient.js +280 -0
- package/package.json +37 -0
package/README.md
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
## n8n PRONOTE nodes
|
|
2
|
+
|
|
3
|
+
Custom n8n nodes and small agent helpers that talk to PRONOTE using the same Pawnote flow as the mobile app (mobile user-agent, device UUID, `loginCredentials` / `loginToken` + `createSessionHandle`).
|
|
4
|
+
|
|
5
|
+
### Available resources
|
|
6
|
+
- Timetable: fetches the week for a reference date (defaults to today) and groups classes by day.
|
|
7
|
+
- Tasks: homework for the week of a reference date, with an `onlyPending` toggle.
|
|
8
|
+
- Grades: list available periods or pull an overview (student/class averages plus detailed grades per subject).
|
|
9
|
+
|
|
10
|
+
### Installation
|
|
11
|
+
1) `cd n8n-node-pronote`
|
|
12
|
+
2) Install deps: `npm install`
|
|
13
|
+
3) Build: `npm run build`
|
|
14
|
+
4) Copy/symlink the `dist` folder into your n8n custom nodes directory, or publish the package to a registry and install it in n8n.
|
|
15
|
+
|
|
16
|
+
### Credentials
|
|
17
|
+
Configure the **PRONOTE API** credentials in n8n:
|
|
18
|
+
- Authentication mode: `Username and Password` or `Existing Mobile Token` (the `mdp` token from a prior mobile login).
|
|
19
|
+
- Instance URL: full PRONOTE base URL (the node normalizes it with Pawnote `cleanURL`).
|
|
20
|
+
- Device UUID: optional but recommended so the instance does not ask to re-validate a new device every run.
|
|
21
|
+
- Account kind: Student/Parent/Teacher (defaults to Student).
|
|
22
|
+
|
|
23
|
+
### Agent helpers
|
|
24
|
+
See `agent-tools/pronoteTools.ts` for a thin wrapper you can drop into LLM agent stacks. It reuses the same login and fetch functions as the node.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type GradeOverviewResponse, type GradePeriodInfo, type HomeworkItem, type PronoteLoginOptions, type TimetableDay } from "../shared/pawnoteClient";
|
|
2
|
+
export interface PronoteAgentTools {
|
|
3
|
+
getTimetable: (referenceDate?: Date | string) => Promise<TimetableDay[]>;
|
|
4
|
+
getTasks: (referenceDate?: Date | string) => Promise<HomeworkItem[]>;
|
|
5
|
+
getGrades: (periodName?: string) => Promise<GradeOverviewResponse>;
|
|
6
|
+
getGradePeriods: () => Promise<GradePeriodInfo[]>;
|
|
7
|
+
}
|
|
8
|
+
export declare function createPronoteAgent(options: PronoteLoginOptions): Promise<PronoteAgentTools>;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createPronoteAgent = createPronoteAgent;
|
|
4
|
+
const pawnoteClient_1 = require("../shared/pawnoteClient");
|
|
5
|
+
async function createPronoteAgent(options) {
|
|
6
|
+
const auth = await (0, pawnoteClient_1.createPronoteSession)(options);
|
|
7
|
+
return {
|
|
8
|
+
getTimetable: (referenceDate) => (0, pawnoteClient_1.fetchTimetable)(auth.session, normalizeDate(referenceDate)),
|
|
9
|
+
getTasks: (referenceDate) => (0, pawnoteClient_1.fetchAssignments)(auth.session, normalizeDate(referenceDate)),
|
|
10
|
+
getGrades: (periodName) => (0, pawnoteClient_1.fetchGrades)(auth.session, periodName),
|
|
11
|
+
getGradePeriods: () => (0, pawnoteClient_1.fetchGradePeriods)(auth.session),
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
function normalizeDate(referenceDate) {
|
|
15
|
+
if (!referenceDate) {
|
|
16
|
+
return new Date();
|
|
17
|
+
}
|
|
18
|
+
const date = typeof referenceDate === "string" ? new Date(referenceDate) : referenceDate;
|
|
19
|
+
if (Number.isNaN(date.getTime())) {
|
|
20
|
+
throw new Error(`Invalid reference date: ${referenceDate}`);
|
|
21
|
+
}
|
|
22
|
+
return date;
|
|
23
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.PronoteApi = void 0;
|
|
4
|
+
class PronoteApi {
|
|
5
|
+
constructor() {
|
|
6
|
+
this.name = "pronoteApi";
|
|
7
|
+
this.displayName = "PRONOTE API";
|
|
8
|
+
this.documentationUrl = "https://pawnote.docs.literate.ink/";
|
|
9
|
+
this.properties = [
|
|
10
|
+
{
|
|
11
|
+
displayName: "Authentication Mode",
|
|
12
|
+
name: "authMode",
|
|
13
|
+
type: "options",
|
|
14
|
+
default: "credentials",
|
|
15
|
+
options: [
|
|
16
|
+
{
|
|
17
|
+
name: "Username and Password",
|
|
18
|
+
value: "credentials",
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
name: "Existing Mobile Token",
|
|
22
|
+
value: "token",
|
|
23
|
+
},
|
|
24
|
+
],
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
displayName: "Instance URL",
|
|
28
|
+
name: "instanceUrl",
|
|
29
|
+
type: "string",
|
|
30
|
+
default: "",
|
|
31
|
+
placeholder: "https://example.index-education.net/pronote",
|
|
32
|
+
description: "Base URL of the PRONOTE instance",
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
displayName: "Username",
|
|
36
|
+
name: "username",
|
|
37
|
+
type: "string",
|
|
38
|
+
default: "",
|
|
39
|
+
description: "PRONOTE username",
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
displayName: "Password",
|
|
43
|
+
name: "password",
|
|
44
|
+
type: "string",
|
|
45
|
+
typeOptions: {
|
|
46
|
+
password: true,
|
|
47
|
+
},
|
|
48
|
+
default: "",
|
|
49
|
+
description: "PRONOTE password",
|
|
50
|
+
displayOptions: {
|
|
51
|
+
show: {
|
|
52
|
+
authMode: ["credentials"],
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
displayName: "Mobile Token",
|
|
58
|
+
name: "token",
|
|
59
|
+
type: "string",
|
|
60
|
+
typeOptions: {
|
|
61
|
+
password: true,
|
|
62
|
+
},
|
|
63
|
+
default: "",
|
|
64
|
+
description: "Mobile token (mdp) obtained from a prior mobile login",
|
|
65
|
+
displayOptions: {
|
|
66
|
+
show: {
|
|
67
|
+
authMode: ["token"],
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
displayName: "Device UUID",
|
|
73
|
+
name: "deviceUUID",
|
|
74
|
+
type: "string",
|
|
75
|
+
default: "",
|
|
76
|
+
description: "Optional persistent device UUID to reuse across logins",
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
displayName: "Account Kind",
|
|
80
|
+
name: "kind",
|
|
81
|
+
type: "options",
|
|
82
|
+
default: "STUDENT",
|
|
83
|
+
options: [
|
|
84
|
+
{
|
|
85
|
+
name: "Student",
|
|
86
|
+
value: "STUDENT",
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
name: "Parent",
|
|
90
|
+
value: "PARENT",
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
name: "Teacher",
|
|
94
|
+
value: "TEACHER",
|
|
95
|
+
},
|
|
96
|
+
],
|
|
97
|
+
},
|
|
98
|
+
];
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
exports.PronoteApi = PronoteApi;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { IExecuteFunctions, ILoadOptionsFunctions } from "n8n-workflow";
|
|
2
|
+
import { type PronoteAuthContext } from "../../shared/pawnoteClient";
|
|
3
|
+
type Context = IExecuteFunctions | ILoadOptionsFunctions;
|
|
4
|
+
export declare function getPronoteAuth(context: Context, itemIndex?: number): Promise<PronoteAuthContext>;
|
|
5
|
+
export declare function resolveReferenceDate(raw?: string): Date;
|
|
6
|
+
export {};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getPronoteAuth = getPronoteAuth;
|
|
4
|
+
exports.resolveReferenceDate = resolveReferenceDate;
|
|
5
|
+
const pawnoteClient_1 = require("../../shared/pawnoteClient");
|
|
6
|
+
async function getPronoteAuth(context, itemIndex = 0) {
|
|
7
|
+
const credentials = (await context.getCredentials("pronoteApi"));
|
|
8
|
+
if (!credentials) {
|
|
9
|
+
throw new Error("Pronote credentials are missing. Please configure the Pronote API credentials.");
|
|
10
|
+
}
|
|
11
|
+
return (0, pawnoteClient_1.createPronoteSession)({
|
|
12
|
+
instanceUrl: credentials.instanceUrl,
|
|
13
|
+
username: credentials.username,
|
|
14
|
+
password: credentials.password,
|
|
15
|
+
token: credentials.token,
|
|
16
|
+
deviceUUID: credentials.deviceUUID,
|
|
17
|
+
kind: credentials.kind,
|
|
18
|
+
authMode: credentials.authMode,
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
function resolveReferenceDate(raw) {
|
|
22
|
+
if (!raw) {
|
|
23
|
+
return new Date();
|
|
24
|
+
}
|
|
25
|
+
const date = new Date(raw);
|
|
26
|
+
if (Number.isNaN(date.getTime())) {
|
|
27
|
+
throw new Error(`Invalid date provided: ${raw}`);
|
|
28
|
+
}
|
|
29
|
+
return date;
|
|
30
|
+
}
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.Pronote = void 0;
|
|
4
|
+
const pawnoteClient_1 = require("../../shared/pawnoteClient");
|
|
5
|
+
const GenericFunctions_1 = require("./GenericFunctions");
|
|
6
|
+
class Pronote {
|
|
7
|
+
constructor() {
|
|
8
|
+
this.description = {
|
|
9
|
+
displayName: "PRONOTE",
|
|
10
|
+
name: "pronote",
|
|
11
|
+
group: ["transform"],
|
|
12
|
+
icon: "file:pronote.svg",
|
|
13
|
+
version: 1,
|
|
14
|
+
subtitle: "={{ $parameter[\"operation\"] + \": \" + $parameter[\"resource\"] }}",
|
|
15
|
+
description: "Interact with PRONOTE using the Pawnote client",
|
|
16
|
+
defaults: {
|
|
17
|
+
name: "PRONOTE",
|
|
18
|
+
},
|
|
19
|
+
inputs: ["main"],
|
|
20
|
+
outputs: ["main"],
|
|
21
|
+
credentials: [
|
|
22
|
+
{
|
|
23
|
+
name: "pronoteApi",
|
|
24
|
+
required: true,
|
|
25
|
+
},
|
|
26
|
+
],
|
|
27
|
+
properties: [
|
|
28
|
+
{
|
|
29
|
+
displayName: "Resource",
|
|
30
|
+
name: "resource",
|
|
31
|
+
type: "options",
|
|
32
|
+
options: [
|
|
33
|
+
{
|
|
34
|
+
name: "Timetable",
|
|
35
|
+
value: "timetable",
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
name: "Tasks",
|
|
39
|
+
value: "tasks",
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
name: "Grades",
|
|
43
|
+
value: "grades",
|
|
44
|
+
},
|
|
45
|
+
],
|
|
46
|
+
default: "timetable",
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
displayName: "Operation",
|
|
50
|
+
name: "operation",
|
|
51
|
+
type: "options",
|
|
52
|
+
displayOptions: {
|
|
53
|
+
show: {
|
|
54
|
+
resource: ["timetable"],
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
options: [
|
|
58
|
+
{
|
|
59
|
+
name: "Get Week",
|
|
60
|
+
value: "get",
|
|
61
|
+
description: "Fetch the timetable for the week of a reference date",
|
|
62
|
+
action: "Get timetable",
|
|
63
|
+
},
|
|
64
|
+
],
|
|
65
|
+
default: "get",
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
displayName: "Reference Date",
|
|
69
|
+
name: "referenceDate",
|
|
70
|
+
type: "string",
|
|
71
|
+
placeholder: "2024-09-01",
|
|
72
|
+
default: "",
|
|
73
|
+
description: "Date used to compute the PRONOTE week (ISO 8601). Defaults to today.",
|
|
74
|
+
displayOptions: {
|
|
75
|
+
show: {
|
|
76
|
+
resource: ["timetable"],
|
|
77
|
+
operation: ["get"],
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
displayName: "Operation",
|
|
83
|
+
name: "operation",
|
|
84
|
+
type: "options",
|
|
85
|
+
displayOptions: {
|
|
86
|
+
show: {
|
|
87
|
+
resource: ["tasks"],
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
options: [
|
|
91
|
+
{
|
|
92
|
+
name: "Get Week",
|
|
93
|
+
value: "get",
|
|
94
|
+
description: "Fetch homework/tasks for the week of a reference date",
|
|
95
|
+
action: "Get tasks",
|
|
96
|
+
},
|
|
97
|
+
],
|
|
98
|
+
default: "get",
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
displayName: "Reference Date",
|
|
102
|
+
name: "referenceDate",
|
|
103
|
+
type: "string",
|
|
104
|
+
placeholder: "2024-09-01",
|
|
105
|
+
default: "",
|
|
106
|
+
description: "Date used to compute the PRONOTE week (ISO 8601). Defaults to today.",
|
|
107
|
+
displayOptions: {
|
|
108
|
+
show: {
|
|
109
|
+
resource: ["tasks"],
|
|
110
|
+
operation: ["get"],
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
displayName: "Only Pending",
|
|
116
|
+
name: "onlyPending",
|
|
117
|
+
type: "boolean",
|
|
118
|
+
default: false,
|
|
119
|
+
description: "If enabled, completed tasks are filtered out",
|
|
120
|
+
displayOptions: {
|
|
121
|
+
show: {
|
|
122
|
+
resource: ["tasks"],
|
|
123
|
+
operation: ["get"],
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
displayName: "Operation",
|
|
129
|
+
name: "operation",
|
|
130
|
+
type: "options",
|
|
131
|
+
displayOptions: {
|
|
132
|
+
show: {
|
|
133
|
+
resource: ["grades"],
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
options: [
|
|
137
|
+
{
|
|
138
|
+
name: "Get Overview",
|
|
139
|
+
value: "get",
|
|
140
|
+
description: "Fetch averages and detailed grades for a period",
|
|
141
|
+
action: "Get grades",
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
name: "List Periods",
|
|
145
|
+
value: "listPeriods",
|
|
146
|
+
description: "List all available grade periods",
|
|
147
|
+
action: "List grade periods",
|
|
148
|
+
},
|
|
149
|
+
],
|
|
150
|
+
default: "get",
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
displayName: "Period Name or ID",
|
|
154
|
+
name: "periodName",
|
|
155
|
+
type: "string",
|
|
156
|
+
default: "",
|
|
157
|
+
description: "Name or ID of the grade period. Defaults to the first available period.",
|
|
158
|
+
displayOptions: {
|
|
159
|
+
show: {
|
|
160
|
+
resource: ["grades"],
|
|
161
|
+
operation: ["get"],
|
|
162
|
+
},
|
|
163
|
+
},
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
displayName: "Include Individual Grades",
|
|
167
|
+
name: "includeGrades",
|
|
168
|
+
type: "boolean",
|
|
169
|
+
default: true,
|
|
170
|
+
description: "Return individual grade entries along with averages",
|
|
171
|
+
displayOptions: {
|
|
172
|
+
show: {
|
|
173
|
+
resource: ["grades"],
|
|
174
|
+
operation: ["get"],
|
|
175
|
+
},
|
|
176
|
+
},
|
|
177
|
+
},
|
|
178
|
+
],
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
async execute() {
|
|
182
|
+
const items = this.getInputData();
|
|
183
|
+
const returnData = [];
|
|
184
|
+
let auth = null;
|
|
185
|
+
const ensureAuth = async () => {
|
|
186
|
+
if (!auth) {
|
|
187
|
+
auth = await (0, GenericFunctions_1.getPronoteAuth)(this);
|
|
188
|
+
}
|
|
189
|
+
return auth;
|
|
190
|
+
};
|
|
191
|
+
for (let i = 0; i < items.length; i++) {
|
|
192
|
+
const resource = this.getNodeParameter("resource", i);
|
|
193
|
+
const operation = this.getNodeParameter("operation", i);
|
|
194
|
+
const { session } = await ensureAuth();
|
|
195
|
+
if (resource === "timetable") {
|
|
196
|
+
if (operation !== "get") {
|
|
197
|
+
throw new Error(`Unsupported operation ${operation} for timetable resource.`);
|
|
198
|
+
}
|
|
199
|
+
const dateInput = this.getNodeParameter("referenceDate", i);
|
|
200
|
+
const referenceDate = (0, GenericFunctions_1.resolveReferenceDate)(dateInput);
|
|
201
|
+
const timetable = await (0, pawnoteClient_1.fetchTimetable)(session, referenceDate);
|
|
202
|
+
for (const day of timetable) {
|
|
203
|
+
returnData.push({ json: day });
|
|
204
|
+
}
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
if (resource === "tasks") {
|
|
208
|
+
if (operation !== "get") {
|
|
209
|
+
throw new Error(`Unsupported operation ${operation} for tasks resource.`);
|
|
210
|
+
}
|
|
211
|
+
const dateInput = this.getNodeParameter("referenceDate", i);
|
|
212
|
+
const onlyPending = this.getNodeParameter("onlyPending", i);
|
|
213
|
+
const referenceDate = (0, GenericFunctions_1.resolveReferenceDate)(dateInput);
|
|
214
|
+
const assignments = await (0, pawnoteClient_1.fetchAssignments)(session, referenceDate);
|
|
215
|
+
for (const assignment of assignments) {
|
|
216
|
+
if (onlyPending && assignment.isDone) {
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
returnData.push({ json: assignment });
|
|
220
|
+
}
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
if (resource === "grades") {
|
|
224
|
+
if (operation === "listPeriods") {
|
|
225
|
+
const periods = await (0, pawnoteClient_1.fetchGradePeriods)(session);
|
|
226
|
+
for (const period of periods) {
|
|
227
|
+
returnData.push({ json: period });
|
|
228
|
+
}
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
if (operation === "get") {
|
|
232
|
+
const periodName = this.getNodeParameter("periodName", i).trim() || undefined;
|
|
233
|
+
const includeGrades = this.getNodeParameter("includeGrades", i);
|
|
234
|
+
const overview = await (0, pawnoteClient_1.fetchGrades)(session, periodName);
|
|
235
|
+
if (!includeGrades) {
|
|
236
|
+
returnData.push({
|
|
237
|
+
json: {
|
|
238
|
+
period: overview.period,
|
|
239
|
+
studentOverall: overview.studentOverall,
|
|
240
|
+
classAverage: overview.classAverage,
|
|
241
|
+
subjects: overview.subjects.map((subject) => ({
|
|
242
|
+
id: subject.id,
|
|
243
|
+
name: subject.name,
|
|
244
|
+
studentAverage: subject.studentAverage,
|
|
245
|
+
classAverage: subject.classAverage,
|
|
246
|
+
maximum: subject.maximum,
|
|
247
|
+
minimum: subject.minimum,
|
|
248
|
+
outOf: subject.outOf,
|
|
249
|
+
})),
|
|
250
|
+
},
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
else {
|
|
254
|
+
returnData.push({ json: overview });
|
|
255
|
+
}
|
|
256
|
+
continue;
|
|
257
|
+
}
|
|
258
|
+
throw new Error(`Unsupported operation ${operation} for grades resource.`);
|
|
259
|
+
}
|
|
260
|
+
throw new Error(`Unknown resource ${resource}.`);
|
|
261
|
+
}
|
|
262
|
+
return [returnData];
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
exports.Pronote = Pronote;
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { AccountKind, type SessionHandle } from "pawnote";
|
|
2
|
+
export declare const pronoteUserAgent = "Mozilla/5.0 (iPhone; CPU iPhone OS 19_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 PRONOTE Mobile APP Version/2.0.11";
|
|
3
|
+
export type PronoteAuthMode = "credentials" | "token";
|
|
4
|
+
export interface PronoteLoginOptions {
|
|
5
|
+
instanceUrl: string;
|
|
6
|
+
username: string;
|
|
7
|
+
password?: string;
|
|
8
|
+
token?: string;
|
|
9
|
+
deviceUUID?: string;
|
|
10
|
+
kind?: AccountKind | keyof typeof AccountKind;
|
|
11
|
+
authMode?: PronoteAuthMode;
|
|
12
|
+
}
|
|
13
|
+
export interface PronoteAuthContext {
|
|
14
|
+
session: SessionHandle;
|
|
15
|
+
token: string;
|
|
16
|
+
username: string;
|
|
17
|
+
kind: AccountKind;
|
|
18
|
+
url: string;
|
|
19
|
+
deviceUUID: string;
|
|
20
|
+
}
|
|
21
|
+
export interface TimetableCourse {
|
|
22
|
+
id: string;
|
|
23
|
+
type: "lesson" | "detention" | "activity";
|
|
24
|
+
subject?: string;
|
|
25
|
+
title?: string;
|
|
26
|
+
room?: string;
|
|
27
|
+
teacher?: string;
|
|
28
|
+
group?: string;
|
|
29
|
+
status?: string;
|
|
30
|
+
from: string;
|
|
31
|
+
to: string;
|
|
32
|
+
notes?: string;
|
|
33
|
+
resourceId?: string;
|
|
34
|
+
backgroundColor?: string;
|
|
35
|
+
[key: string]: unknown;
|
|
36
|
+
}
|
|
37
|
+
export interface TimetableDay {
|
|
38
|
+
date: string;
|
|
39
|
+
courses: TimetableCourse[];
|
|
40
|
+
[key: string]: unknown;
|
|
41
|
+
}
|
|
42
|
+
export interface HomeworkAttachment {
|
|
43
|
+
name: string;
|
|
44
|
+
url: string;
|
|
45
|
+
type: string | number;
|
|
46
|
+
[key: string]: unknown;
|
|
47
|
+
}
|
|
48
|
+
export interface HomeworkItem {
|
|
49
|
+
id: string;
|
|
50
|
+
subject: string;
|
|
51
|
+
description: string;
|
|
52
|
+
dueDate: string;
|
|
53
|
+
isDone: boolean;
|
|
54
|
+
returnKind: string;
|
|
55
|
+
attachments: HomeworkAttachment[];
|
|
56
|
+
[key: string]: unknown;
|
|
57
|
+
}
|
|
58
|
+
export interface GradePeriodInfo {
|
|
59
|
+
id: string;
|
|
60
|
+
name: string;
|
|
61
|
+
start: string;
|
|
62
|
+
end: string;
|
|
63
|
+
[key: string]: unknown;
|
|
64
|
+
}
|
|
65
|
+
export interface GradeScore {
|
|
66
|
+
value: number;
|
|
67
|
+
disabled?: boolean;
|
|
68
|
+
status?: string;
|
|
69
|
+
[key: string]: unknown;
|
|
70
|
+
}
|
|
71
|
+
export interface GradeEntry {
|
|
72
|
+
id: string;
|
|
73
|
+
subjectId: string;
|
|
74
|
+
subjectName: string;
|
|
75
|
+
description?: string;
|
|
76
|
+
givenAt: string;
|
|
77
|
+
bonus: boolean;
|
|
78
|
+
optional: boolean;
|
|
79
|
+
outOf: GradeScore;
|
|
80
|
+
coefficient?: number;
|
|
81
|
+
studentScore: GradeScore;
|
|
82
|
+
averageScore: GradeScore;
|
|
83
|
+
minScore: GradeScore;
|
|
84
|
+
maxScore: GradeScore;
|
|
85
|
+
[key: string]: unknown;
|
|
86
|
+
}
|
|
87
|
+
export interface SubjectGrades {
|
|
88
|
+
id: string;
|
|
89
|
+
name: string;
|
|
90
|
+
studentAverage: GradeScore;
|
|
91
|
+
classAverage: GradeScore;
|
|
92
|
+
maximum: GradeScore;
|
|
93
|
+
minimum: GradeScore;
|
|
94
|
+
outOf: GradeScore;
|
|
95
|
+
grades: GradeEntry[];
|
|
96
|
+
[key: string]: unknown;
|
|
97
|
+
}
|
|
98
|
+
export interface GradeOverviewResponse {
|
|
99
|
+
period: GradePeriodInfo;
|
|
100
|
+
studentOverall: GradeScore;
|
|
101
|
+
classAverage: GradeScore;
|
|
102
|
+
subjects: SubjectGrades[];
|
|
103
|
+
[key: string]: unknown;
|
|
104
|
+
}
|
|
105
|
+
export declare function createPronoteSession(options: PronoteLoginOptions): Promise<PronoteAuthContext>;
|
|
106
|
+
export declare function fetchTimetable(session: SessionHandle, referenceDate: Date): Promise<TimetableDay[]>;
|
|
107
|
+
export declare function fetchAssignments(session: SessionHandle, referenceDate: Date): Promise<HomeworkItem[]>;
|
|
108
|
+
export declare function fetchGradePeriods(session: SessionHandle): Promise<GradePeriodInfo[]>;
|
|
109
|
+
export declare function fetchGrades(session: SessionHandle, periodName?: string): Promise<GradeOverviewResponse>;
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.pronoteUserAgent = void 0;
|
|
4
|
+
exports.createPronoteSession = createPronoteSession;
|
|
5
|
+
exports.fetchTimetable = fetchTimetable;
|
|
6
|
+
exports.fetchAssignments = fetchAssignments;
|
|
7
|
+
exports.fetchGradePeriods = fetchGradePeriods;
|
|
8
|
+
exports.fetchGrades = fetchGrades;
|
|
9
|
+
const crypto_1 = require("crypto");
|
|
10
|
+
const pawnote_1 = require("pawnote");
|
|
11
|
+
exports.pronoteUserAgent = "Mozilla/5.0 (iPhone; CPU iPhone OS 19_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 PRONOTE Mobile APP Version/2.0.11";
|
|
12
|
+
const pawnoteFetcher = async (options) => {
|
|
13
|
+
const response = await fetch(options.url, {
|
|
14
|
+
method: options.method,
|
|
15
|
+
headers: {
|
|
16
|
+
...options.headers,
|
|
17
|
+
"User-Agent": exports.pronoteUserAgent,
|
|
18
|
+
},
|
|
19
|
+
body: options.method !== "GET" ? options.content : undefined,
|
|
20
|
+
redirect: options.redirect,
|
|
21
|
+
});
|
|
22
|
+
const content = await response.text();
|
|
23
|
+
return {
|
|
24
|
+
content,
|
|
25
|
+
status: response.status,
|
|
26
|
+
get headers() {
|
|
27
|
+
return response.headers;
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
};
|
|
31
|
+
async function createPronoteSession(options) {
|
|
32
|
+
const url = (0, pawnote_1.cleanURL)(options.instanceUrl);
|
|
33
|
+
const authMode = options.authMode ?? "credentials";
|
|
34
|
+
const deviceUUID = ensureDeviceUUID(options.deviceUUID);
|
|
35
|
+
const kind = normalizeKind(options.kind);
|
|
36
|
+
const session = (0, pawnote_1.createSessionHandle)(pawnoteFetcher);
|
|
37
|
+
if (authMode === "token") {
|
|
38
|
+
if (!options.token) {
|
|
39
|
+
throw new Error("Missing PRONOTE token for token-based login.");
|
|
40
|
+
}
|
|
41
|
+
const auth = await (0, pawnote_1.loginToken)(session, {
|
|
42
|
+
url,
|
|
43
|
+
kind,
|
|
44
|
+
username: options.username,
|
|
45
|
+
token: options.token,
|
|
46
|
+
deviceUUID,
|
|
47
|
+
});
|
|
48
|
+
return {
|
|
49
|
+
session,
|
|
50
|
+
token: auth.token,
|
|
51
|
+
username: auth.username,
|
|
52
|
+
kind: auth.kind,
|
|
53
|
+
url: auth.url,
|
|
54
|
+
deviceUUID,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
if (!options.password) {
|
|
58
|
+
throw new Error("Missing password for credential-based login.");
|
|
59
|
+
}
|
|
60
|
+
const auth = await (0, pawnote_1.loginCredentials)(session, {
|
|
61
|
+
url,
|
|
62
|
+
kind,
|
|
63
|
+
username: options.username,
|
|
64
|
+
password: options.password,
|
|
65
|
+
deviceUUID,
|
|
66
|
+
});
|
|
67
|
+
return {
|
|
68
|
+
session,
|
|
69
|
+
token: auth.token,
|
|
70
|
+
username: auth.username,
|
|
71
|
+
kind: auth.kind,
|
|
72
|
+
url: auth.url,
|
|
73
|
+
deviceUUID,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
async function fetchTimetable(session, referenceDate) {
|
|
77
|
+
const weekNumber = (0, pawnote_1.translateToWeekNumber)(referenceDate, session.instance.firstMonday);
|
|
78
|
+
const timetable = await (0, pawnote_1.timetableFromWeek)(session, weekNumber);
|
|
79
|
+
(0, pawnote_1.parseTimetable)(session, timetable, {
|
|
80
|
+
withSuperposedCanceledClasses: false,
|
|
81
|
+
withCanceledClasses: true,
|
|
82
|
+
withPlannedClasses: true,
|
|
83
|
+
});
|
|
84
|
+
const mappedCourses = timetable.classes.map(mapCourse);
|
|
85
|
+
const grouped = {};
|
|
86
|
+
for (const course of mappedCourses) {
|
|
87
|
+
const dayKey = course.from.split("T")[0];
|
|
88
|
+
if (!grouped[dayKey]) {
|
|
89
|
+
grouped[dayKey] = [];
|
|
90
|
+
}
|
|
91
|
+
grouped[dayKey].push(course);
|
|
92
|
+
}
|
|
93
|
+
return Object.entries(grouped)
|
|
94
|
+
.sort(([a], [b]) => new Date(a).getTime() - new Date(b).getTime())
|
|
95
|
+
.map(([date, courses]) => ({
|
|
96
|
+
date,
|
|
97
|
+
courses: courses.sort((a, b) => new Date(a.from).getTime() - new Date(b.from).getTime()),
|
|
98
|
+
}));
|
|
99
|
+
}
|
|
100
|
+
async function fetchAssignments(session, referenceDate) {
|
|
101
|
+
const weekNumber = (0, pawnote_1.translateToWeekNumber)(referenceDate, session.instance.firstMonday);
|
|
102
|
+
const assignments = await (0, pawnote_1.assignmentsFromWeek)(session, weekNumber);
|
|
103
|
+
return assignments.map((assignment) => ({
|
|
104
|
+
id: assignment.id,
|
|
105
|
+
subject: assignment.subject.name,
|
|
106
|
+
description: assignment.description,
|
|
107
|
+
dueDate: serializeDate(assignment.deadline),
|
|
108
|
+
isDone: assignment.done,
|
|
109
|
+
returnKind: assignment.return.kind === 1 ? "paper" : "upload",
|
|
110
|
+
attachments: assignment.attachments.map((attachment) => ({
|
|
111
|
+
type: attachment.kind,
|
|
112
|
+
name: attachment.name,
|
|
113
|
+
url: attachment.url,
|
|
114
|
+
})),
|
|
115
|
+
}));
|
|
116
|
+
}
|
|
117
|
+
async function fetchGradePeriods(session) {
|
|
118
|
+
const gradeTab = session.user.resources[0].tabs.get(pawnote_1.TabLocation.Grades);
|
|
119
|
+
if (!gradeTab) {
|
|
120
|
+
throw new Error("Grades tab not available for this account.");
|
|
121
|
+
}
|
|
122
|
+
return gradeTab.periods.map((period) => ({
|
|
123
|
+
id: period.id,
|
|
124
|
+
name: period.name,
|
|
125
|
+
start: serializeDate(period.startDate),
|
|
126
|
+
end: serializeDate(period.endDate),
|
|
127
|
+
}));
|
|
128
|
+
}
|
|
129
|
+
async function fetchGrades(session, periodName) {
|
|
130
|
+
const gradeTab = session.user.resources[0].tabs.get(pawnote_1.TabLocation.Grades);
|
|
131
|
+
if (!gradeTab) {
|
|
132
|
+
throw new Error("Grades tab not available for this account.");
|
|
133
|
+
}
|
|
134
|
+
const period = pickPeriod(gradeTab.periods, periodName);
|
|
135
|
+
const overview = await (0, pawnote_1.gradesOverview)(session, period);
|
|
136
|
+
return {
|
|
137
|
+
period: {
|
|
138
|
+
id: period.id,
|
|
139
|
+
name: period.name,
|
|
140
|
+
start: serializeDate(period.startDate),
|
|
141
|
+
end: serializeDate(period.endDate),
|
|
142
|
+
},
|
|
143
|
+
studentOverall: mapGradeValueToScore(overview.overallAverage),
|
|
144
|
+
classAverage: mapGradeValueToScore(overview.classAverage),
|
|
145
|
+
subjects: mapSubjects(overview),
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
function normalizeKind(kind) {
|
|
149
|
+
if (typeof kind === "number") {
|
|
150
|
+
return kind;
|
|
151
|
+
}
|
|
152
|
+
if (typeof kind === "string") {
|
|
153
|
+
const key = kind.toUpperCase();
|
|
154
|
+
if (pawnote_1.AccountKind[key]) {
|
|
155
|
+
return pawnote_1.AccountKind[key];
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return pawnote_1.AccountKind.STUDENT;
|
|
159
|
+
}
|
|
160
|
+
function ensureDeviceUUID(deviceUUID) {
|
|
161
|
+
if (deviceUUID && deviceUUID.trim().length > 0) {
|
|
162
|
+
return deviceUUID.trim();
|
|
163
|
+
}
|
|
164
|
+
try {
|
|
165
|
+
return (0, crypto_1.randomUUID)();
|
|
166
|
+
}
|
|
167
|
+
catch {
|
|
168
|
+
return Math.random().toString(16).slice(2);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
function serializeDate(date) {
|
|
172
|
+
return date.toISOString();
|
|
173
|
+
}
|
|
174
|
+
function mapCourse(course) {
|
|
175
|
+
const base = {
|
|
176
|
+
id: course.id,
|
|
177
|
+
from: serializeDate(course.startDate),
|
|
178
|
+
to: serializeDate(course.endDate),
|
|
179
|
+
notes: course.notes,
|
|
180
|
+
backgroundColor: course.backgroundColor,
|
|
181
|
+
};
|
|
182
|
+
if (course.is === "lesson") {
|
|
183
|
+
return {
|
|
184
|
+
...base,
|
|
185
|
+
type: "lesson",
|
|
186
|
+
subject: course.subject?.name,
|
|
187
|
+
room: course.classrooms.join(", "),
|
|
188
|
+
teacher: course.teacherNames.join(", "),
|
|
189
|
+
group: course.groupNames.join(", "),
|
|
190
|
+
status: course.status,
|
|
191
|
+
resourceId: course.lessonResourceID,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
if (course.is === "detention") {
|
|
195
|
+
return {
|
|
196
|
+
...base,
|
|
197
|
+
type: "detention",
|
|
198
|
+
title: course.title ?? "Detention",
|
|
199
|
+
room: course.classrooms.join(", "),
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
return {
|
|
203
|
+
...base,
|
|
204
|
+
type: "activity",
|
|
205
|
+
title: course.title,
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
function mapSubjects(overview) {
|
|
209
|
+
const grades = overview.grades.map((grade) => mapGrade(grade));
|
|
210
|
+
const subjects = [];
|
|
211
|
+
for (const subjectAverage of overview.subjectsAverages) {
|
|
212
|
+
const subjectGrades = grades.filter((g) => g.subjectId === subjectAverage.subject.id);
|
|
213
|
+
subjects.push({
|
|
214
|
+
id: subjectAverage.subject.id,
|
|
215
|
+
name: subjectAverage.subject.name,
|
|
216
|
+
studentAverage: mapGradeValueToScore(subjectAverage.student),
|
|
217
|
+
classAverage: mapGradeValueToScore(subjectAverage.class_average),
|
|
218
|
+
maximum: mapGradeValueToScore(subjectAverage.max),
|
|
219
|
+
minimum: mapGradeValueToScore(subjectAverage.min),
|
|
220
|
+
outOf: mapGradeValueToScore(subjectAverage.outOf),
|
|
221
|
+
grades: subjectGrades,
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
return subjects;
|
|
225
|
+
}
|
|
226
|
+
function mapGrade(grade) {
|
|
227
|
+
return {
|
|
228
|
+
id: grade.id,
|
|
229
|
+
subjectId: grade.subject.id,
|
|
230
|
+
subjectName: grade.subject.name,
|
|
231
|
+
description: grade.comment,
|
|
232
|
+
givenAt: serializeDate(grade.date),
|
|
233
|
+
bonus: grade.isBonus ?? false,
|
|
234
|
+
optional: grade.isOptional ?? false,
|
|
235
|
+
outOf: mapGradeValueToScore(grade.outOf),
|
|
236
|
+
coefficient: grade.coefficient,
|
|
237
|
+
studentScore: mapGradeValueToScore(grade.value),
|
|
238
|
+
averageScore: mapGradeValueToScore(grade.average),
|
|
239
|
+
minScore: mapGradeValueToScore(grade.min),
|
|
240
|
+
maxScore: mapGradeValueToScore(grade.max),
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
function mapGradeValueToScore(grade) {
|
|
244
|
+
if (typeof grade === "undefined") {
|
|
245
|
+
return { value: 0, disabled: true, status: "unknown" };
|
|
246
|
+
}
|
|
247
|
+
switch (grade.kind) {
|
|
248
|
+
case pawnote_1.GradeKind.Grade:
|
|
249
|
+
return { value: grade.points ?? 0 };
|
|
250
|
+
case pawnote_1.GradeKind.NotGraded:
|
|
251
|
+
return { value: 0, disabled: true, status: "not graded" };
|
|
252
|
+
case pawnote_1.GradeKind.Absent:
|
|
253
|
+
return { value: 0, disabled: true, status: "absent" };
|
|
254
|
+
case pawnote_1.GradeKind.AbsentZero:
|
|
255
|
+
return { value: 0, disabled: false, status: "absent zero" };
|
|
256
|
+
case pawnote_1.GradeKind.Exempted:
|
|
257
|
+
return { value: 0, disabled: true, status: "exempted" };
|
|
258
|
+
case pawnote_1.GradeKind.Unfit:
|
|
259
|
+
return { value: 0, disabled: true, status: "unfit" };
|
|
260
|
+
case pawnote_1.GradeKind.Unreturned:
|
|
261
|
+
return { value: 0, disabled: true, status: "not returned" };
|
|
262
|
+
case pawnote_1.GradeKind.UnreturnedZero:
|
|
263
|
+
return { value: 0, disabled: false, status: "not returned zero" };
|
|
264
|
+
default:
|
|
265
|
+
return { value: 0, disabled: true, status: "unknown" };
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
function pickPeriod(periods, periodName) {
|
|
269
|
+
if (!periods.length) {
|
|
270
|
+
throw new Error("No grade periods are available for this account.");
|
|
271
|
+
}
|
|
272
|
+
if (!periodName) {
|
|
273
|
+
return periods[0];
|
|
274
|
+
}
|
|
275
|
+
const period = periods.find((p) => p.name.toLowerCase() === periodName.toLowerCase() || p.id === periodName);
|
|
276
|
+
if (!period) {
|
|
277
|
+
throw new Error(`Grade period "${periodName}" was not found.`);
|
|
278
|
+
}
|
|
279
|
+
return period;
|
|
280
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "n8n-nodes-pronote",
|
|
3
|
+
"version": "0.1.2",
|
|
4
|
+
"description": "n8n custom nodes and agent utilities to talk to PRONOTE via the Pawnote library.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"main": "dist/nodes/Pronote/Pronote.node.js",
|
|
7
|
+
"types": "dist/nodes/Pronote/Pronote.node.d.ts",
|
|
8
|
+
"scripts": {
|
|
9
|
+
"build": "tsc -p .",
|
|
10
|
+
"clean": "rimraf dist"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"dist"
|
|
14
|
+
],
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"pawnote": "^1.6.2"
|
|
17
|
+
},
|
|
18
|
+
"peerDependencies": {
|
|
19
|
+
"n8n-core": "^1.0.0",
|
|
20
|
+
"n8n-workflow": "^1.0.0"
|
|
21
|
+
},
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"@types/node": "^22.7.4",
|
|
24
|
+
"n8n-core": "^1.0.0",
|
|
25
|
+
"n8n-workflow": "^1.0.0",
|
|
26
|
+
"rimraf": "^6.0.1",
|
|
27
|
+
"typescript": "^5.6.2"
|
|
28
|
+
},
|
|
29
|
+
"n8n": {
|
|
30
|
+
"nodes": [
|
|
31
|
+
"dist/nodes/Pronote/Pronote.node.js"
|
|
32
|
+
],
|
|
33
|
+
"credentials": [
|
|
34
|
+
"dist/credentials/PronoteApi.credentials.js"
|
|
35
|
+
]
|
|
36
|
+
}
|
|
37
|
+
}
|