@studentsphere/ots-provider-wigor 1.0.2 → 2.0.1
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 +1 -42
- package/dist/constants.d.ts +5 -0
- package/dist/constants.js +36 -0
- package/dist/constants.js.map +1 -0
- package/dist/index.d.ts +3 -26
- package/dist/index.js +245 -598
- package/dist/index.js.map +1 -1
- package/dist/schools.d.ts +2 -0
- package/dist/schools.js +143 -0
- package/dist/schools.js.map +1 -0
- package/dist/types.d.ts +38 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/utils.d.ts +6 -0
- package/dist/utils.js +14 -0
- package/dist/utils.js.map +1 -0
- package/package.json +9 -17
- package/src/index.ts +382 -773
package/src/index.ts
CHANGED
|
@@ -1,781 +1,390 @@
|
|
|
1
|
-
import * as crypto from "node:crypto";
|
|
2
1
|
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
2
|
+
BaseTimetableProvider,
|
|
3
|
+
type Course,
|
|
4
|
+
type ProviderCredentials,
|
|
5
|
+
type School,
|
|
7
6
|
} from "@studentsphere/ots-core";
|
|
8
|
-
import
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
"3a",
|
|
18
|
-
"epsi",
|
|
19
|
-
"esail",
|
|
20
|
-
"icl",
|
|
21
|
-
"idrac-business-school",
|
|
22
|
-
"ieft",
|
|
23
|
-
"iet",
|
|
24
|
-
"ifag",
|
|
25
|
-
"igefi",
|
|
26
|
-
"ihedrea",
|
|
27
|
-
"ileri",
|
|
28
|
-
"sup-de-com",
|
|
29
|
-
"viva-mundi",
|
|
30
|
-
"wis",
|
|
31
|
-
];
|
|
32
|
-
|
|
33
|
-
const IGENSIA_SCHOOLS = [
|
|
34
|
-
"american-business-college",
|
|
35
|
-
"business-science-institute",
|
|
36
|
-
"cnva",
|
|
37
|
-
"ecm",
|
|
38
|
-
"emi",
|
|
39
|
-
"esa",
|
|
40
|
-
"esam",
|
|
41
|
-
"icd-business-school",
|
|
42
|
-
"igensia-rh",
|
|
43
|
-
"imis",
|
|
44
|
-
"imsi",
|
|
45
|
-
"ipi",
|
|
46
|
-
"iscpa",
|
|
47
|
-
"ismm",
|
|
48
|
-
];
|
|
7
|
+
import {
|
|
8
|
+
CD_SCHOOLS,
|
|
9
|
+
CD_SCHOOLS_TIMETABLE_ENDPOINT,
|
|
10
|
+
IGENSIA_SCHOOLS,
|
|
11
|
+
IGENSIA_SCHOOLS_TIMETABLE_ENDPOINT,
|
|
12
|
+
LOGIN_SERVER_ENDPOINT,
|
|
13
|
+
} from "./constants";
|
|
14
|
+
import { SCHOOLS_DATA } from "./schools";
|
|
15
|
+
import type { WigorEventJSON } from "./types";
|
|
49
16
|
|
|
50
17
|
const getScheduleServer = (schoolId?: string) => {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
};
|
|
59
|
-
|
|
60
|
-
const FRENCH_MONTHS: Record<string, number> = {
|
|
61
|
-
Janvier: 1,
|
|
62
|
-
Février: 2,
|
|
63
|
-
Mars: 3,
|
|
64
|
-
Avril: 4,
|
|
65
|
-
Mai: 5,
|
|
66
|
-
Juin: 6,
|
|
67
|
-
Juillet: 7,
|
|
68
|
-
Août: 8,
|
|
69
|
-
Septembre: 9,
|
|
70
|
-
Octobre: 10,
|
|
71
|
-
Novembre: 11,
|
|
72
|
-
Décembre: 12,
|
|
18
|
+
if (schoolId && CD_SCHOOLS.includes(schoolId)) {
|
|
19
|
+
return CD_SCHOOLS_TIMETABLE_ENDPOINT;
|
|
20
|
+
}
|
|
21
|
+
if (schoolId && IGENSIA_SCHOOLS.includes(schoolId)) {
|
|
22
|
+
return IGENSIA_SCHOOLS_TIMETABLE_ENDPOINT;
|
|
23
|
+
}
|
|
24
|
+
return CD_SCHOOLS_TIMETABLE_ENDPOINT;
|
|
73
25
|
};
|
|
74
26
|
|
|
75
|
-
const FRENCH_DAYS: Record<string, number> = {
|
|
76
|
-
Lundi: 1,
|
|
77
|
-
Mardi: 2,
|
|
78
|
-
Mercredi: 3,
|
|
79
|
-
Jeudi: 4,
|
|
80
|
-
Vendredi: 5,
|
|
81
|
-
Samedi: 6,
|
|
82
|
-
Dimanche: 0,
|
|
83
|
-
};
|
|
84
|
-
|
|
85
|
-
const WEEK_DAYS = [
|
|
86
|
-
"Sunday",
|
|
87
|
-
"Monday",
|
|
88
|
-
"Tuesday",
|
|
89
|
-
"Wednesday",
|
|
90
|
-
"Thursday",
|
|
91
|
-
"Friday",
|
|
92
|
-
"Saturday",
|
|
93
|
-
];
|
|
94
|
-
|
|
95
|
-
export interface WigorEvent {
|
|
96
|
-
title: string;
|
|
97
|
-
instructor: string;
|
|
98
|
-
program: string;
|
|
99
|
-
startTime: string;
|
|
100
|
-
endTime: string;
|
|
101
|
-
duration: number;
|
|
102
|
-
weekDay: string;
|
|
103
|
-
classroom: string | null;
|
|
104
|
-
campus: string | null;
|
|
105
|
-
deliveryMode: string;
|
|
106
|
-
color: string;
|
|
107
|
-
classGroup: string;
|
|
108
|
-
hash: string;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
27
|
export class WigorProvider extends BaseTimetableProvider {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
"Content-Type": "application/x-www-form-urlencoded",
|
|
475
|
-
"User-Agent": "nodejs-client",
|
|
476
|
-
Accept: "text/html",
|
|
477
|
-
},
|
|
478
|
-
maxRedirects: 0,
|
|
479
|
-
validateStatus: (s) => s >= 200 && s < 400,
|
|
480
|
-
});
|
|
481
|
-
|
|
482
|
-
return client;
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
private isErrorPage(html: string): boolean {
|
|
486
|
-
return (
|
|
487
|
-
html.includes("<title>Error 500</title>") ||
|
|
488
|
-
html.includes("<h1>500</h1>") ||
|
|
489
|
-
html.includes("Unexpected Error")
|
|
490
|
-
);
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
private async fetchEDTHtml(
|
|
494
|
-
client: AxiosInstance,
|
|
495
|
-
scheduleServer: string,
|
|
496
|
-
query: Record<string, string>,
|
|
497
|
-
maxRetries: number = 3,
|
|
498
|
-
): Promise<string> {
|
|
499
|
-
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
500
|
-
try {
|
|
501
|
-
const params = new URLSearchParams(query).toString();
|
|
502
|
-
const url = `${scheduleServer}?${params}`;
|
|
503
|
-
|
|
504
|
-
const res = await client.get(url, {
|
|
505
|
-
headers: { "User-Agent": "nodejs-client", Accept: "text/html" },
|
|
506
|
-
});
|
|
507
|
-
const html = res.data as string;
|
|
508
|
-
if (!this.isErrorPage(html)) {
|
|
509
|
-
return html;
|
|
510
|
-
}
|
|
511
|
-
} catch (_err) {
|
|
512
|
-
// Ignore error and retry
|
|
513
|
-
}
|
|
514
|
-
if (attempt < maxRetries) {
|
|
515
|
-
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
516
|
-
}
|
|
517
|
-
}
|
|
518
|
-
throw new Error("Failed to fetch EDT");
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
private capitalizeName(name: string): string {
|
|
522
|
-
return name
|
|
523
|
-
.split(" ")
|
|
524
|
-
.map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
|
|
525
|
-
.join(" ");
|
|
526
|
-
}
|
|
527
|
-
|
|
528
|
-
private parseFrenchDate(
|
|
529
|
-
dayText: string,
|
|
530
|
-
baseYear: number,
|
|
531
|
-
queryDateObj: Date,
|
|
532
|
-
): string {
|
|
533
|
-
const parts = dayText.trim().split(/\s+/);
|
|
534
|
-
if (parts.length < 3) throw new Error("Invalid date format");
|
|
535
|
-
|
|
536
|
-
const dayName = parts[0];
|
|
537
|
-
if (!dayName || !(dayName in FRENCH_DAYS))
|
|
538
|
-
throw new Error("Invalid day name");
|
|
539
|
-
|
|
540
|
-
const dayNumStr = parts[1];
|
|
541
|
-
if (!dayNumStr) throw new Error("Missing day number");
|
|
542
|
-
const dayNum = parseInt(dayNumStr, 10);
|
|
543
|
-
if (Number.isNaN(dayNum)) throw new Error("Invalid day number");
|
|
544
|
-
|
|
545
|
-
const monthNameRaw = parts[2];
|
|
546
|
-
if (!monthNameRaw) throw new Error("Missing month name");
|
|
547
|
-
const monthName =
|
|
548
|
-
monthNameRaw.charAt(0).toUpperCase() +
|
|
549
|
-
monthNameRaw.slice(1).toLowerCase();
|
|
550
|
-
const month = FRENCH_MONTHS[monthName];
|
|
551
|
-
if (!month) throw new Error("Invalid month name");
|
|
552
|
-
|
|
553
|
-
const weekday = FRENCH_DAYS[dayName];
|
|
554
|
-
|
|
555
|
-
let bestDate: Date | null = null;
|
|
556
|
-
let minDiff = Infinity;
|
|
557
|
-
for (const y of [baseYear - 1, baseYear, baseYear + 1]) {
|
|
558
|
-
const d = new Date(Date.UTC(y, month - 1, dayNum));
|
|
559
|
-
if (d.getUTCDay() === weekday) {
|
|
560
|
-
const diff = Math.abs(d.getTime() - queryDateObj.getTime());
|
|
561
|
-
if (diff < minDiff) {
|
|
562
|
-
minDiff = diff;
|
|
563
|
-
bestDate = d;
|
|
564
|
-
}
|
|
565
|
-
}
|
|
566
|
-
}
|
|
567
|
-
|
|
568
|
-
if (!bestDate) throw new Error("No matching date found");
|
|
569
|
-
const iso = bestDate.toISOString();
|
|
570
|
-
const datePart = iso.split("T")[0];
|
|
571
|
-
if (!datePart) throw new Error("Invalid ISO date format");
|
|
572
|
-
return datePart;
|
|
573
|
-
}
|
|
574
|
-
|
|
575
|
-
private getMonday(date: Date): Date {
|
|
576
|
-
const d = new Date(date);
|
|
577
|
-
const day = d.getDay();
|
|
578
|
-
const diff = (day === 0 ? -6 : 1) - day;
|
|
579
|
-
d.setDate(d.getDate() + diff);
|
|
580
|
-
return d;
|
|
581
|
-
}
|
|
582
|
-
|
|
583
|
-
private addDays(date: Date, days: number): Date {
|
|
584
|
-
const d = new Date(date);
|
|
585
|
-
d.setDate(d.getDate() + days);
|
|
586
|
-
return d;
|
|
587
|
-
}
|
|
588
|
-
|
|
589
|
-
private calculateDuration(
|
|
590
|
-
startTime: string,
|
|
591
|
-
endTime: string,
|
|
592
|
-
date: string,
|
|
593
|
-
): number {
|
|
594
|
-
const start = new Date(`${date}T${startTime}:00Z`);
|
|
595
|
-
const end = new Date(`${date}T${endTime}:00Z`);
|
|
596
|
-
return (end.getTime() - start.getTime()) / (1000 * 60);
|
|
597
|
-
}
|
|
598
|
-
|
|
599
|
-
private parseEdtHtml(html: string, queryDate: string): WigorEvent[] {
|
|
600
|
-
const $ = cheerio.load(html);
|
|
601
|
-
const events: WigorEvent[] = [];
|
|
602
|
-
const queryParts = queryDate.split("/");
|
|
603
|
-
if (queryParts.length !== 3) throw new Error("Invalid query date format");
|
|
604
|
-
|
|
605
|
-
const baseYearStr = queryParts[2];
|
|
606
|
-
if (!baseYearStr) throw new Error("Missing year in query date");
|
|
607
|
-
const baseYear = parseInt(baseYearStr, 10);
|
|
608
|
-
if (Number.isNaN(baseYear)) throw new Error("Invalid year in query date");
|
|
609
|
-
|
|
610
|
-
const monthStr = queryParts[0];
|
|
611
|
-
const dayStr = queryParts[1];
|
|
612
|
-
if (!monthStr || !dayStr)
|
|
613
|
-
throw new Error("Missing month or day in query date");
|
|
614
|
-
const month = parseInt(monthStr, 10);
|
|
615
|
-
const day = parseInt(dayStr, 10);
|
|
616
|
-
if (Number.isNaN(month) || Number.isNaN(day))
|
|
617
|
-
throw new Error("Invalid month or day in query date");
|
|
618
|
-
|
|
619
|
-
const queryDateObj = new Date(Date.UTC(baseYear, month - 1, day));
|
|
620
|
-
|
|
621
|
-
const dayMap: Map<number, string> = new Map();
|
|
622
|
-
$(".Jour").each((_, el) => {
|
|
623
|
-
const $el = $(el);
|
|
624
|
-
const style = $el.attr("style") || "";
|
|
625
|
-
const leftMatch = style.match(/left:([\d.]+)%/);
|
|
626
|
-
if (!leftMatch || !leftMatch[1]) {
|
|
627
|
-
return;
|
|
628
|
-
}
|
|
629
|
-
const left = parseFloat(leftMatch[1]);
|
|
630
|
-
if (left < 100 || left >= 200) {
|
|
631
|
-
return;
|
|
632
|
-
}
|
|
633
|
-
const dayText = $el.find(".TCJour").text().trim();
|
|
634
|
-
try {
|
|
635
|
-
const parsedDate = this.parseFrenchDate(
|
|
636
|
-
dayText,
|
|
637
|
-
baseYear,
|
|
638
|
-
queryDateObj,
|
|
639
|
-
);
|
|
640
|
-
dayMap.set(left, parsedDate);
|
|
641
|
-
} catch (_err) {
|
|
642
|
-
// Skip invalid days
|
|
643
|
-
}
|
|
644
|
-
});
|
|
645
|
-
|
|
646
|
-
const sortedDays = Array.from(dayMap.keys()).sort((a, b) => a - b);
|
|
647
|
-
|
|
648
|
-
$(".Case").each((_, element) => {
|
|
649
|
-
const $case = $(element);
|
|
650
|
-
const style = $case.attr("style") || "";
|
|
651
|
-
const leftMatch = style.match(/left:([\d.]+)%/);
|
|
652
|
-
if (!leftMatch || !leftMatch[1]) {
|
|
653
|
-
return;
|
|
654
|
-
}
|
|
655
|
-
const caseLeft = parseFloat(leftMatch[1]);
|
|
656
|
-
if (caseLeft < 100 || caseLeft >= 200) {
|
|
657
|
-
return;
|
|
658
|
-
}
|
|
659
|
-
|
|
660
|
-
const dayLeft = sortedDays.filter((l) => l <= caseLeft).pop();
|
|
661
|
-
if (dayLeft === undefined) {
|
|
662
|
-
return;
|
|
663
|
-
}
|
|
664
|
-
const eventDate = dayMap.get(dayLeft);
|
|
665
|
-
if (!eventDate) {
|
|
666
|
-
return;
|
|
667
|
-
}
|
|
668
|
-
|
|
669
|
-
const time = $case.find(".TChdeb").text().trim().split(" - ");
|
|
670
|
-
if (time.length !== 2 || !time[0] || !time[1]) {
|
|
671
|
-
return;
|
|
672
|
-
}
|
|
673
|
-
|
|
674
|
-
const profContents = $case
|
|
675
|
-
.find(".TCProf")
|
|
676
|
-
.contents()
|
|
677
|
-
.filter(function () {
|
|
678
|
-
return this.type === "text" && $(this).text().trim() !== "";
|
|
679
|
-
})
|
|
680
|
-
.map(function () {
|
|
681
|
-
return $(this).text().trim();
|
|
682
|
-
})
|
|
683
|
-
.get();
|
|
684
|
-
|
|
685
|
-
const classroomInfo = $case.find(".TCSalle").text().trim();
|
|
686
|
-
|
|
687
|
-
const courseName = $case
|
|
688
|
-
.find("td.TCase")
|
|
689
|
-
.contents()
|
|
690
|
-
.filter(function () {
|
|
691
|
-
return this.type === "text" && $(this).text().trim() !== "";
|
|
692
|
-
})
|
|
693
|
-
.text()
|
|
694
|
-
.trim();
|
|
695
|
-
|
|
696
|
-
const borderColor =
|
|
697
|
-
$case
|
|
698
|
-
.find(".innerCase")
|
|
699
|
-
.attr("style")
|
|
700
|
-
?.match(/border:3px solid\s*([^;]+)/)?.[1] || "";
|
|
701
|
-
|
|
702
|
-
if (!courseName) {
|
|
703
|
-
return;
|
|
704
|
-
}
|
|
705
|
-
|
|
706
|
-
const specParts = profContents[1] ? profContents[1].split(" - ") : [];
|
|
707
|
-
|
|
708
|
-
let classroom: string | null =
|
|
709
|
-
classroomInfo.replace("Salle:", "").split("(")[0]?.trim() || null;
|
|
710
|
-
let campus: string | null =
|
|
711
|
-
classroomInfo.match(/\(([^)]+)\)/)?.[1] || null;
|
|
712
|
-
let sessionType: string = "in_person";
|
|
713
|
-
|
|
714
|
-
if (
|
|
715
|
-
classroomInfo.includes("(DISTANCIEL)") ||
|
|
716
|
-
classroomInfo.includes("Aucune")
|
|
717
|
-
) {
|
|
718
|
-
classroom = null;
|
|
719
|
-
campus = null;
|
|
720
|
-
if (classroomInfo.includes("(DISTANCIEL)")) sessionType = "remote";
|
|
721
|
-
}
|
|
722
|
-
|
|
723
|
-
const dateObj = new Date(eventDate);
|
|
724
|
-
if (Number.isNaN(dateObj.getTime()))
|
|
725
|
-
throw new Error("Invalid event date");
|
|
726
|
-
|
|
727
|
-
const weekDay = WEEK_DAYS[dateObj.getUTCDay()];
|
|
728
|
-
if (!weekDay) throw new Error("Invalid weekday");
|
|
729
|
-
|
|
730
|
-
const startTime = `${eventDate}T${time[0]}:00`;
|
|
731
|
-
const endTime = `${eventDate}T${time[1]}:00`;
|
|
732
|
-
const instructor = profContents[0]
|
|
733
|
-
? this.capitalizeName(profContents[0])
|
|
734
|
-
: "";
|
|
735
|
-
|
|
736
|
-
// Generate stable hash from content
|
|
737
|
-
// We use a combination of stable fields to ensure the same course gives the same hash
|
|
738
|
-
const hashContent = `${courseName}|${startTime}|${endTime}|${classroom}|${instructor}`;
|
|
739
|
-
const hash = crypto
|
|
740
|
-
.createHash("sha256")
|
|
741
|
-
.update(hashContent)
|
|
742
|
-
.digest("hex");
|
|
743
|
-
|
|
744
|
-
const event: WigorEvent = {
|
|
745
|
-
title: courseName.replace(/(\n|\t|\s\s+)/g, " ").trim(),
|
|
746
|
-
instructor,
|
|
747
|
-
program: specParts[1]?.trim() || "",
|
|
748
|
-
startTime,
|
|
749
|
-
endTime,
|
|
750
|
-
duration: this.calculateDuration(time[0], time[1], eventDate),
|
|
751
|
-
weekDay,
|
|
752
|
-
classroom,
|
|
753
|
-
campus: campus || "Arras",
|
|
754
|
-
deliveryMode: sessionType,
|
|
755
|
-
color: borderColor || "#808080",
|
|
756
|
-
classGroup: specParts[0]?.trim() || "",
|
|
757
|
-
hash,
|
|
758
|
-
};
|
|
759
|
-
events.push(event);
|
|
760
|
-
});
|
|
761
|
-
|
|
762
|
-
return events;
|
|
763
|
-
}
|
|
764
|
-
|
|
765
|
-
private areEventsValid(events: WigorEvent[]): boolean {
|
|
766
|
-
return events.every((event) => {
|
|
767
|
-
return (
|
|
768
|
-
event.title &&
|
|
769
|
-
event.title.trim() !== "" &&
|
|
770
|
-
event.instructor &&
|
|
771
|
-
event.instructor.trim() !== "" &&
|
|
772
|
-
event.startTime &&
|
|
773
|
-
event.startTime.trim() !== "" &&
|
|
774
|
-
event.endTime &&
|
|
775
|
-
event.endTime.trim() !== "" &&
|
|
776
|
-
event.weekDay &&
|
|
777
|
-
event.weekDay.trim() !== ""
|
|
778
|
-
);
|
|
779
|
-
});
|
|
780
|
-
}
|
|
28
|
+
get id(): string {
|
|
29
|
+
return "wigor";
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
get name(): string {
|
|
33
|
+
return "Wigor";
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
get logo(): string {
|
|
37
|
+
return "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADUAAAA1CAYAAADh5qNwAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAIvSURBVHgB7Zm/SgNBEMZXCSRoCBaJWAQMxCo2Cip2Wltrrz6JDyDYCLa+g2ArgpUWNiaVwQipTAoJMUQQlDHZsH/mLpfdFZwwv+puMnc3385+e5tk5uzm4FtMGbNiCmFRVGBRVGBRVGBRVGBRVGBRVGBRVEgJB3ZWDsV6cU+LXdwdic+vDyt3f+1EFBcqo/N6+15cPZ1aebnMojjePtdij81rcft8KSbFqVPN95oVUwtXKWRLRt5qorzBc6rCBUdR9sOwYiGWTs1psXRqHhVQzm8gz7EHLwlOomCatboNoRe1aeUVssvo9VhXTaFwf2w6J8F5oTBHMZcp/HZBpZzfQq81BwD8ZIpy7RIQTBRgFotNMyyOda7efhCueIiyfaUWi/lJAh1VPYiJMqf3JDiLgvluClM7FbUaSlS/mYsMzAJXPwFeL99W91U7V30VtXRL5ACAn+A6/b4N4YOXKGzey2LHd6oUmefjJ8CzUw0rBkViXTJzpa9C+wnwEoX5KqpQbLuDDYCvnwDvDS32vqos7WqxTv9tWGxPi8NUDe0nILgowCxU5sBmVgV7j/n6CfAWNdjO9GJzpKgkuwTXTayKt6jBPvAlNkcWOq5gn62RSpAviXHFgJ86/dbwuDU6xu/j3yXgz0WZn8UV/s86VY30lS2qFnufEAT7jSLKV2ahUatbqC4BwURhxap+ksDCgvnKXO59mOE/sonAoqjAoqjAoqjAoqjAoqjAoqjAojwAxN68XM4/01cAAAAAElFTkSuQmCC";
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
get schools(): School[] {
|
|
41
|
+
return SCHOOLS_DATA;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async validateCredentials(
|
|
45
|
+
credentials: ProviderCredentials,
|
|
46
|
+
): Promise<boolean> {
|
|
47
|
+
try {
|
|
48
|
+
const { cookies } = await this.login(LOGIN_SERVER_ENDPOINT, credentials);
|
|
49
|
+
return cookies.size > 0;
|
|
50
|
+
} catch (_err) {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async getSchedule(
|
|
56
|
+
credentials: ProviderCredentials,
|
|
57
|
+
from: Date,
|
|
58
|
+
to: Date,
|
|
59
|
+
): Promise<Course[]> {
|
|
60
|
+
const scheduleServer = getScheduleServer(
|
|
61
|
+
credentials.schoolId as string | undefined,
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
const params = new URLSearchParams({
|
|
65
|
+
sort: "",
|
|
66
|
+
group: "",
|
|
67
|
+
filter: "",
|
|
68
|
+
dateDebut: from.toISOString(),
|
|
69
|
+
dateFin: to.toISOString(),
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
const fullScheduleUrl = `${scheduleServer}?${params.toString()}`;
|
|
73
|
+
|
|
74
|
+
const { cookies } = await this.login(
|
|
75
|
+
LOGIN_SERVER_ENDPOINT,
|
|
76
|
+
credentials,
|
|
77
|
+
fullScheduleUrl,
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
const response = await fetch(fullScheduleUrl, {
|
|
81
|
+
headers: {
|
|
82
|
+
"User-Agent": "ots",
|
|
83
|
+
Cookie: this.serializeCookies(cookies, scheduleServer),
|
|
84
|
+
},
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
if (!response.ok || response.status === 302) {
|
|
88
|
+
const text = await response.text();
|
|
89
|
+
if (text.includes("cas/login") || response.status === 302) {
|
|
90
|
+
throw new Error(
|
|
91
|
+
"Session expired or redirected to login. Authentication was not fully established.",
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
throw new Error(
|
|
95
|
+
`Failed to fetch schedule: ${response.status} ${response.statusText}`,
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const contentType = response.headers.get("content-type");
|
|
100
|
+
if (!contentType?.includes("application/json")) {
|
|
101
|
+
const text = await response.text();
|
|
102
|
+
console.error("[Wigor] Expected JSON but received:", text.slice(0, 200));
|
|
103
|
+
throw new Error(
|
|
104
|
+
`Expected JSON response but received ${contentType || "unknown content type"}. You might have been redirected to a login page.`,
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const json = (await response.json()) as { Data: WigorEventJSON[] };
|
|
109
|
+
const events = json.Data || [];
|
|
110
|
+
|
|
111
|
+
return events.map((event) => {
|
|
112
|
+
const subject =
|
|
113
|
+
event.Commentaire && event.Commentaire !== "COMMENTAIRE"
|
|
114
|
+
? event.Commentaire
|
|
115
|
+
: event.Matiere || event.Title || "Sans titre";
|
|
116
|
+
|
|
117
|
+
return {
|
|
118
|
+
hash: event.NoCours.toString(),
|
|
119
|
+
subject,
|
|
120
|
+
start: new Date(event.Start),
|
|
121
|
+
end: new Date(event.End),
|
|
122
|
+
location:
|
|
123
|
+
event.Salles && event.Salles !== "Aucune" ? event.Salles : undefined,
|
|
124
|
+
teacher: event.NomProf || "Anonyme",
|
|
125
|
+
color: `rgb(${event.ColorRed},${event.ColorGreen},${event.ColorBlue})`,
|
|
126
|
+
description: (() => {
|
|
127
|
+
const parts: string[] = [];
|
|
128
|
+
parts.push(`\n`);
|
|
129
|
+
const teamsUrlField = event.TeamsURL || event.TeamsUrl;
|
|
130
|
+
if (teamsUrlField) {
|
|
131
|
+
const linkRegex =
|
|
132
|
+
/<a [^>]*href="([^"]+)"[^>]*>[\s\S]*?<img [^>]*src="([^"]+)"[^>]*>[\s\S]*?<\/a>/gi;
|
|
133
|
+
let match: RegExpExecArray | null;
|
|
134
|
+
// biome-ignore lint/suspicious/noAssignInExpressions: valid regex usage
|
|
135
|
+
while ((match = linkRegex.exec(teamsUrlField)) !== null) {
|
|
136
|
+
if (match[1] && match[2]) {
|
|
137
|
+
const url = match[1];
|
|
138
|
+
const imgSrc = match[2];
|
|
139
|
+
const labelMatch = imgSrc.match(/MTeams_([^.]+)\.png/);
|
|
140
|
+
const label = labelMatch
|
|
141
|
+
? `Lien Teams ${labelMatch[1].replace(/_/g, " ")}`
|
|
142
|
+
: "Lien Teams";
|
|
143
|
+
parts.push(`${label}:\n${url}\n`);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (parts.length <= 1) {
|
|
148
|
+
// 1 because of the initial newline
|
|
149
|
+
const simpleUrls = [
|
|
150
|
+
...teamsUrlField.matchAll(/href="([^"]+)"/g),
|
|
151
|
+
].map((m) => m[1]);
|
|
152
|
+
for (const url of simpleUrls) {
|
|
153
|
+
if (url) parts.push(`Lien Teams:\n${url}\n`);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (event.LibelleGroupe) {
|
|
159
|
+
parts.push(event.LibelleGroupe);
|
|
160
|
+
}
|
|
161
|
+
if (event.LibelleSemaine) {
|
|
162
|
+
parts.push(event.LibelleSemaine);
|
|
163
|
+
}
|
|
164
|
+
if (event.Commentaire && event.Commentaire !== "COMMENTAIRE") {
|
|
165
|
+
parts.push(event.Commentaire);
|
|
166
|
+
}
|
|
167
|
+
if (event.Description) {
|
|
168
|
+
parts.push(event.Description);
|
|
169
|
+
}
|
|
170
|
+
return parts.length > 0 ? parts.join("\n") : undefined;
|
|
171
|
+
})(),
|
|
172
|
+
originalData: event,
|
|
173
|
+
};
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
private async login(
|
|
178
|
+
loginServer: string,
|
|
179
|
+
credentials: ProviderCredentials,
|
|
180
|
+
service?: string,
|
|
181
|
+
): Promise<{
|
|
182
|
+
cookies: Map<
|
|
183
|
+
string,
|
|
184
|
+
Map<string, { value: string; path: string; expires?: number }>
|
|
185
|
+
>;
|
|
186
|
+
}> {
|
|
187
|
+
const jar: Map<
|
|
188
|
+
string,
|
|
189
|
+
Map<string, { value: string; path: string; expires?: number }>
|
|
190
|
+
> = new Map();
|
|
191
|
+
const serviceParam = service
|
|
192
|
+
? `?service=${encodeURIComponent(service)}`
|
|
193
|
+
: "";
|
|
194
|
+
|
|
195
|
+
let currentUrl = `${loginServer}${serviceParam}`;
|
|
196
|
+
|
|
197
|
+
// 1. GET login page to extract hidden fields
|
|
198
|
+
const getRes = await fetch(currentUrl, {
|
|
199
|
+
headers: { "User-Agent": "ots" },
|
|
200
|
+
});
|
|
201
|
+
this.updateCookies(jar, currentUrl, getRes.headers.getSetCookie());
|
|
202
|
+
const html = await getRes.text();
|
|
203
|
+
const hidden = this.extractHiddenFields(html);
|
|
204
|
+
|
|
205
|
+
// 2. POST credentials
|
|
206
|
+
const form = new URLSearchParams();
|
|
207
|
+
form.append("username", credentials.identifier as string);
|
|
208
|
+
form.append("password", credentials.password || "");
|
|
209
|
+
for (const [k, v] of Object.entries(hidden)) {
|
|
210
|
+
if (k !== "username" && k !== "password") form.append(k, v);
|
|
211
|
+
}
|
|
212
|
+
if (!form.has("_eventId")) form.append("_eventId", "submit");
|
|
213
|
+
let response = await fetch(currentUrl, {
|
|
214
|
+
method: "POST",
|
|
215
|
+
headers: {
|
|
216
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
217
|
+
"User-Agent": "ots",
|
|
218
|
+
Cookie: this.serializeCookies(jar, currentUrl),
|
|
219
|
+
},
|
|
220
|
+
body: form.toString(),
|
|
221
|
+
redirect: "manual",
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
// 3. Follow redirect chain manually to capture all intermediate cookies
|
|
225
|
+
let redirectCount = 0;
|
|
226
|
+
const maxRedirects = 15;
|
|
227
|
+
const seenTickets = new Set<string>();
|
|
228
|
+
|
|
229
|
+
while (true) {
|
|
230
|
+
this.updateCookies(jar, currentUrl, response.headers.getSetCookie());
|
|
231
|
+
|
|
232
|
+
if (response.status >= 300 && response.status < 400) {
|
|
233
|
+
let location = response.headers.get("location");
|
|
234
|
+
if (!location) break;
|
|
235
|
+
|
|
236
|
+
// Ticket Reuse Prevention: detect and strip reused tickets
|
|
237
|
+
const urlObj = new URL(location, currentUrl);
|
|
238
|
+
const ticket = urlObj.searchParams.get("ticket");
|
|
239
|
+
if (ticket) {
|
|
240
|
+
if (seenTickets.has(ticket)) {
|
|
241
|
+
urlObj.searchParams.delete("ticket");
|
|
242
|
+
location = urlObj.toString();
|
|
243
|
+
} else {
|
|
244
|
+
seenTickets.add(ticket);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
currentUrl = new URL(location, currentUrl).toString();
|
|
249
|
+
const cookieHeader = this.serializeCookies(jar, currentUrl);
|
|
250
|
+
if (cookieHeader)
|
|
251
|
+
response = await fetch(currentUrl, {
|
|
252
|
+
headers: {
|
|
253
|
+
"User-Agent": "ots",
|
|
254
|
+
Cookie: cookieHeader,
|
|
255
|
+
},
|
|
256
|
+
redirect: "manual",
|
|
257
|
+
});
|
|
258
|
+
redirectCount++;
|
|
259
|
+
if (redirectCount > maxRedirects) break;
|
|
260
|
+
} else {
|
|
261
|
+
// Final response or error
|
|
262
|
+
if (response.status >= 400) {
|
|
263
|
+
const body = await response.text();
|
|
264
|
+
console.error(
|
|
265
|
+
`[Wigor] [DEBUG] Error Status: ${response.status} at ${currentUrl}`,
|
|
266
|
+
);
|
|
267
|
+
console.error(`[Wigor] [DEBUG] Error Body: ${body.slice(0, 1000)}`);
|
|
268
|
+
}
|
|
269
|
+
break;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (response.status >= 400) {
|
|
274
|
+
throw new Error(
|
|
275
|
+
`Authentication failed with status ${response.status} at ${currentUrl}`,
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
return { cookies: jar };
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
private extractHiddenFields(html: string): Record<string, string> {
|
|
283
|
+
const fields: Record<string, string> = {};
|
|
284
|
+
const regex =
|
|
285
|
+
/<input[^>]+type="hidden"[^>]+name="([^"]+)"[^>]+value="([^"]*)"/gi;
|
|
286
|
+
let match: RegExpExecArray | null;
|
|
287
|
+
// biome-ignore lint/suspicious/noAssignInExpressions: valid regex usage
|
|
288
|
+
while ((match = regex.exec(html)) !== null) {
|
|
289
|
+
if (match[1]) fields[match[1]] = match[2] || "";
|
|
290
|
+
}
|
|
291
|
+
return fields;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
private updateCookies(
|
|
295
|
+
jar: Map<
|
|
296
|
+
string,
|
|
297
|
+
Map<string, { value: string; path: string; expires?: number }>
|
|
298
|
+
>,
|
|
299
|
+
url: string,
|
|
300
|
+
setCookieHeaders: string[],
|
|
301
|
+
) {
|
|
302
|
+
const parsedUrl = new URL(url);
|
|
303
|
+
const hostname = parsedUrl.hostname;
|
|
304
|
+
|
|
305
|
+
for (const header of setCookieHeaders) {
|
|
306
|
+
const parts = header.split(";");
|
|
307
|
+
const firstPart = parts[0]?.split("=");
|
|
308
|
+
|
|
309
|
+
if (firstPart && firstPart.length === 2 && firstPart[0]) {
|
|
310
|
+
const name = firstPart[0].trim();
|
|
311
|
+
const value = firstPart[1].trim();
|
|
312
|
+
|
|
313
|
+
let targetDomain = hostname;
|
|
314
|
+
let path = "/";
|
|
315
|
+
let isDeletion = value === "";
|
|
316
|
+
let expiresAt: number | undefined;
|
|
317
|
+
|
|
318
|
+
for (const part of parts.slice(1)) {
|
|
319
|
+
const p = part.trim().toLowerCase();
|
|
320
|
+
if (p.startsWith("domain=")) {
|
|
321
|
+
const d = p.split("=")[1]?.trim();
|
|
322
|
+
if (d) targetDomain = d.startsWith(".") ? d : `.${d}`;
|
|
323
|
+
} else if (p.startsWith("path=")) {
|
|
324
|
+
path = p.split("=")[1]?.trim() || "/";
|
|
325
|
+
} else if (p.startsWith("expires=")) {
|
|
326
|
+
const exp = p.split("=")[1]?.trim();
|
|
327
|
+
if (exp) {
|
|
328
|
+
const date = new Date(exp);
|
|
329
|
+
if (date.getTime() < Date.now()) isDeletion = true;
|
|
330
|
+
expiresAt = date.getTime();
|
|
331
|
+
}
|
|
332
|
+
} else if (p.startsWith("max-age=")) {
|
|
333
|
+
const maxAge = parseInt(p.split("=")[1]?.trim() || "0", 10);
|
|
334
|
+
if (maxAge <= 0) isDeletion = true;
|
|
335
|
+
else expiresAt = Date.now() + maxAge * 1000;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
let domainCookies = jar.get(targetDomain);
|
|
340
|
+
if (!domainCookies) {
|
|
341
|
+
domainCookies = new Map();
|
|
342
|
+
jar.set(targetDomain, domainCookies);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
if (isDeletion) {
|
|
346
|
+
domainCookies.delete(name);
|
|
347
|
+
} else {
|
|
348
|
+
domainCookies.set(name, { value, path, expires: expiresAt });
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
private serializeCookies(
|
|
355
|
+
jar: Map<
|
|
356
|
+
string,
|
|
357
|
+
Map<string, { value: string; path: string; expires?: number }>
|
|
358
|
+
>,
|
|
359
|
+
url: string,
|
|
360
|
+
): string {
|
|
361
|
+
const parsedUrl = new URL(url);
|
|
362
|
+
const hostname = parsedUrl.hostname;
|
|
363
|
+
const path = parsedUrl.pathname;
|
|
364
|
+
const cookiesToSet = new Map<string, string>();
|
|
365
|
+
|
|
366
|
+
for (const [domain, domainCookies] of jar.entries()) {
|
|
367
|
+
// Send if exact host match or if it's a parent domain wildcard
|
|
368
|
+
if (
|
|
369
|
+
hostname === domain ||
|
|
370
|
+
(domain.startsWith(".") && hostname.endsWith(domain))
|
|
371
|
+
) {
|
|
372
|
+
for (const [name, cookie] of domainCookies.entries()) {
|
|
373
|
+
// Expiry check
|
|
374
|
+
if (cookie.expires && cookie.expires < Date.now()) {
|
|
375
|
+
domainCookies.delete(name);
|
|
376
|
+
continue;
|
|
377
|
+
}
|
|
378
|
+
// Path match: cookie path must be a prefix of request path
|
|
379
|
+
if (path.startsWith(cookie.path)) {
|
|
380
|
+
cookiesToSet.set(name, cookie.value);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
return Array.from(cookiesToSet.entries())
|
|
387
|
+
.map(([name, value]) => `${name}=${value}`)
|
|
388
|
+
.join("; ");
|
|
389
|
+
}
|
|
781
390
|
}
|