Skip to content

Commit 265075e

Browse files
committed
feat: added the attendance and added a way for the restmanager to handle non json response
1 parent 13a8b60 commit 265075e

File tree

5 files changed

+124
-44
lines changed

5 files changed

+124
-44
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,3 +139,4 @@ grades.json
139139
subjects.json
140140
timetable.json
141141
timetable.json
142+
attendance.json

examples/attendance.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
2+
import readline from "readline";
3+
import fs from "fs";
4+
import dotenv from "dotenv";
5+
import { LoginWithToken } from "../src";
6+
7+
dotenv.config();
8+
9+
const rl = readline.createInterface({
10+
input: process.stdin,
11+
output: process.stdout
12+
});
13+
14+
const askQuestion = (query: string): Promise<string> => {
15+
return new Promise(resolve => rl.question(query, resolve));
16+
};
17+
18+
const main = async () => {
19+
try {
20+
const url = process.env.INSTANCE_URL ?? "";
21+
const refreshToken = process.env.REFRESH_TOKEN ?? "";
22+
const userId = process.env.USER_ID ?? "";
23+
const deviceId = process.env.DEVICE_ID ?? "";
24+
25+
// Refresh token and get device ID
26+
console.log("Refreshing token...");
27+
const smartschool = await LoginWithToken(url, refreshToken, deviceId);
28+
console.log("Token refreshed successfully.");
29+
30+
// Fetch attendance items
31+
console.log("\nFetching attendance items...");
32+
const attendanceItems = await smartschool.GetAttendanceItems();
33+
34+
fs.writeFileSync("attendance.json", JSON.stringify(attendanceItems, null, 2));
35+
console.log("\nAttendance items fetched successfully:");
36+
console.log(`Found ${attendanceItems.length} attendance items.`);
37+
console.log(JSON.stringify(attendanceItems, null, 2));
38+
} catch (error) {
39+
console.error("\nAn error occurred:");
40+
console.error(error);
41+
} finally {
42+
rl.close();
43+
}
44+
};
45+
46+
main();

src/rest/RESTManager.ts

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ export class RestManager {
99
this.baseURL = baseURL;
1010
}
1111

12-
private async sendRequest<T>(options: RequestOptions): Promise<T> {
12+
private async sendRequest<T>(options: RequestOptions,textonly?: boolean): Promise<T> {
1313
const { method, path, body, headers } = options;
1414
const url = `${this.baseURL}/${path}`;
1515

@@ -34,18 +34,19 @@ export class RestManager {
3434
});
3535

3636
if (!response.ok) {
37-
const responseData = await response.json();
37+
const responseData = await response.text();
3838
console.error(`Error response from ${method} ${url}:`, responseData);
3939
console.error("Request Headers:", headers);
4040
console.error("Request Body:", body);
4141
throw new Error(`${response.status}: ${JSON.stringify(responseData)}`);
4242
}
43-
44-
const responseData = await response.json();
45-
// console.log("Body:", body);
46-
// console.log("Response Data:", responseData);
47-
// console.log("headers:", headers);
48-
return responseData as T;
43+
if (textonly === undefined || textonly === false) {
44+
const responseData = await response.json();
45+
return responseData as T;
46+
}else {
47+
const responseData = await response.text();
48+
return responseData as unknown as T;
49+
}
4950
}
5051

5152
async delete<T>(path: string, params?: Record<string, any>, options?: RequestOptions): Promise<T> {
@@ -69,7 +70,7 @@ export class RestManager {
6970
});
7071
}
7172

72-
async post<T>(path: string, body: any, params?: Record<string, any>, options?: RequestOptions): Promise<T> {
73+
async post<T>(path: string, body: any, params?: Record<string, any>, options?: RequestOptions,textonly?: boolean): Promise<T> {
7374
const urlParams = new URLSearchParams(params).toString();
7475
const urlPath = urlParams ? `${path}?${urlParams}` : path;
7576
console.log("POST URL Params:", urlParams);
@@ -79,7 +80,7 @@ export class RestManager {
7980
path: urlPath,
8081
body,
8182
headers: options?.headers
82-
});
83+
}, textonly);
8384
}
8485

8586
async put<T>(path: string, body: any, options?: RequestOptions): Promise<T> {

src/routes/Attendance.ts

Lines changed: 65 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -5,41 +5,73 @@ import { absenceFileStateIncluded, absenceReasonIncluded } from "../types/Attend
55
import { BaseDataResponse, BaseResponse } from "../types/RequestHandler";
66
import { AttendanceItemState, AttendanceItemType } from "../util/Constants";
77
import { getSingleRelation } from "../util/Relations";
8+
import { extractBaseUrl } from "../util/URL";
89

9-
const manager = new RestManager(BASE_URL());
1010

11-
export const GetAttendanceItems = async (userId: string, accessToken: string): Promise<Array<AttendanceItem>> => {
12-
const response = await manager.get<BaseResponse>(ATTENDANCE_FILES(), {
13-
"filter[student.id]": userId,
14-
"filter[currentState.absenceType]": "ABSENCE,CLATENESS",
15-
"filter[absenceFile]": "currentState",
16-
"include": "currentState,currentState.absenceReason,currentState.absenceRecurrence",
17-
"fields[absenceFileState]": "creationDateTime,absenceStartDateTime,absenceEndDateTime,absenceType,absenceFileStatus,absenceReason,absenceRecurrence",
18-
"fields[absenceReason]": "code,longLabel"
19-
}, {
20-
Authorization: `Bearer ${accessToken}`
21-
});
22-
23-
const includedMap = new Map<string, unknown>();
24-
for (const item of response.included ?? []) {
25-
includedMap.set(`${item.type}:${item.id}`, item);
11+
function parseDate(date: string): Date {
12+
const clean = date.replace(/^\w+\.\s*/, "")
13+
const months: Record<string, number> = {
14+
"janvier": 0, "février": 1, "mars": 2,
15+
"avril": 3, "mai": 4, "juin": 5,
16+
"juillet": 6, "août": 7, "septembre": 8,
17+
"octobre": 9, "novembre": 10, "décembre": 11
18+
};
19+
const match = clean.match(/^(\d{1,2}) (\w+) (\d{4})$/);
20+
if (!match) {
21+
throw new Error(`Invalid date format: ${date}`);
22+
}
23+
const [, day, month, year] = match;
24+
const monthIndex = months[month.toLowerCase()];
25+
if (monthIndex === undefined) {
26+
throw new Error(`Invalid month name: ${month}`);
2627
}
28+
return new Date(Number(year), monthIndex, Number(day));
29+
}
30+
31+
export const GetAttendanceItems = async (url:string, userId: string, accessToken: string, mobileId: string): Promise<Array<AttendanceItem>> => {
32+
const [base] = extractBaseUrl(url);
33+
const manager = new RestManager(base);
34+
35+
const body = new URLSearchParams(
36+
{ pupilID: userId.split("_")[1] }
37+
);
38+
const responsetext = await manager.post<any>(
39+
ATTENDANCE_FILES(),
40+
body,
41+
undefined,
42+
{
43+
headers: {
44+
"Content-Type": "application/x-www-form-urlencoded",
45+
"Authorization": `Bearer ${accessToken}`,
46+
"Accept-Language": "fr",
47+
"SmscMobileId": mobileId
48+
}
49+
},
50+
true
51+
);
2752

28-
return (Array.isArray(response.data) ? response.data : [])
29-
.filter((item): item is BaseDataResponse<"absenceFile"> => item.type === "absenceFile")
30-
.map(absenceItem => {
31-
const fileId = getSingleRelation(absenceItem.relationships.currentState)?.id;
32-
const file = fileId ? includedMap.get("absenceFileState:" + fileId) as absenceFileStateIncluded : null;
33-
const reasonId = getSingleRelation(file?.relationships?.absenceReason)?.id;
34-
const reason = reasonId ? includedMap.get("absenceReason:" + reasonId) as absenceReasonIncluded : null;
35-
return new AttendanceItem(
36-
absenceItem.id,
37-
new Date(file?.attributes?.creationDateTime ?? ""),
38-
new Date(file?.attributes?.absenceStartDateTime ?? ""),
39-
new Date(file?.attributes?.absenceEndDateTime ?? ""),
40-
file?.attributes?.absenceType ?? AttendanceItemType.ABSENCE,
41-
file?.attributes?.absenceFileStatus ?? AttendanceItemState.OPEN,
42-
reason?.attributes?.longLabel ?? ""
43-
);
44-
});
53+
const response = JSON.parse(responsetext);
54+
const attendanceItems: Array<AttendanceItem> = [];
55+
56+
const currentYear = response.year_label;
57+
const currentYearAttendance = response.presences?.[currentYear] || [];
58+
59+
for (const attendance of currentYearAttendance) {
60+
const attendanceData = attendance.am || attendance.pm || attendance.full;
61+
62+
if (attendanceData) {
63+
const date = parseDate(attendanceData.formattedDate);
64+
65+
attendanceItems.push(new AttendanceItem(
66+
attendanceData.codeKey || "",
67+
date,
68+
new Date(date), // Start date
69+
new Date(date), // End date
70+
attendanceData.codeDescr || AttendanceItemType.ABSENCE,
71+
AttendanceItemState.OPEN,
72+
attendanceData.motivation || attendanceData.codeDescr || ""
73+
));
74+
}
75+
}
76+
return attendanceItems;
4577
};

src/structures/Smartschool.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ export class SmartSchool {
5353
}
5454
async GetAttendanceItems(): Promise<Array<AttendanceItem>> {
5555
await this.refreshAccessToken();
56-
return GetAttendanceItems(this.userId, this.accessToken);
56+
return GetAttendanceItems(this.refreshURL, this.userId, this.accessToken, this.SMSCMobileID);
5757
}
5858
async GetGradesForPeriod(period?: string): Promise<Array<Subject>> {
5959
await this.refreshAccessToken();

0 commit comments

Comments
 (0)