vku-sdk 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/README.md +38 -0
- package/package.json +36 -0
- package/pnpm-workspace.yaml +2 -0
- package/src/VkuSDK.ts +184 -0
- package/src/core/auth/index.ts +1 -0
- package/src/core/auth/session-provider.interface.ts +5 -0
- package/src/core/cache/cache.interface.ts +6 -0
- package/src/core/cache/index.ts +2 -0
- package/src/core/cache/infra/file-cache.ts +82 -0
- package/src/core/cache/infra/index.ts +3 -0
- package/src/core/cache/infra/memory-cache.ts +32 -0
- package/src/core/cache/infra/storage-cache.ts +24 -0
- package/src/core/executor/index.ts +1 -0
- package/src/core/executor/request-executor.ts +20 -0
- package/src/core/http/adapters/axios-http-client.ts +25 -0
- package/src/core/http/adapters/http-account.decorator.ts +19 -0
- package/src/core/http/adapters/http-cache.decorator.ts +55 -0
- package/src/core/http/adapters/http-logger.decorator.ts +29 -0
- package/src/core/http/adapters/http-retry.decorator.ts +34 -0
- package/src/core/http/adapters/index.ts +5 -0
- package/src/core/http/base-request.ts +124 -0
- package/src/core/http/http-client.interface.ts +5 -0
- package/src/core/http/index.ts +3 -0
- package/src/core/index.ts +6 -0
- package/src/core/parser/index.ts +1 -0
- package/src/core/parser/parser.interface.ts +3 -0
- package/src/core/utils/html-detect.ts +13 -0
- package/src/core/utils/index.ts +1 -0
- package/src/index.ts +4 -0
- package/src/modules/accounts/domain/account.entity.ts +45 -0
- package/src/modules/accounts/domain/index.ts +1 -0
- package/src/modules/accounts/index.ts +2 -0
- package/src/modules/accounts/infra/account-session.provider.ts +17 -0
- package/src/modules/accounts/infra/account.factory.ts +48 -0
- package/src/modules/accounts/infra/chrome-cdp-login.ts +62 -0
- package/src/modules/accounts/infra/index.ts +3 -0
- package/src/modules/enrollment/domain/class-session.entity.ts +13 -0
- package/src/modules/enrollment/domain/course-section.entity.ts +19 -0
- package/src/modules/enrollment/domain/index.ts +4 -0
- package/src/modules/enrollment/domain/subject-action-response.entity.ts +6 -0
- package/src/modules/enrollment/domain/subject-class.entity.ts +66 -0
- package/src/modules/enrollment/index.ts +3 -0
- package/src/modules/enrollment/parsers/class-session.parser.ts +79 -0
- package/src/modules/enrollment/parsers/index.ts +2 -0
- package/src/modules/enrollment/parsers/subject-class.parser.ts +72 -0
- package/src/modules/enrollment/requests/get-registered-subject-class.request.ts +14 -0
- package/src/modules/enrollment/requests/get-subject-classes.request.ts +28 -0
- package/src/modules/enrollment/requests/index.ts +4 -0
- package/src/modules/enrollment/requests/register-subject-class.request.ts +21 -0
- package/src/modules/enrollment/requests/unregister-subject-class.request.ts +24 -0
- package/src/modules/index.ts +3 -0
- package/src/modules/shared/index.ts +2 -0
- package/src/modules/shared/infra/chrome-wrapper.ts +380 -0
- package/src/modules/shared/infra/index.ts +1 -0
- package/src/modules/shared/parsers/html-table.parser.ts +320 -0
- package/src/modules/shared/parsers/index.ts +1 -0
- package/tsconfig.json +21 -0
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { IParser } from "../parser/parser.interface";
|
|
2
|
+
|
|
3
|
+
export enum HttpMethod {
|
|
4
|
+
GET = "GET",
|
|
5
|
+
POST = "POST",
|
|
6
|
+
PUT = "PUT",
|
|
7
|
+
DELETE = "DELETE",
|
|
8
|
+
PATCH = "PATCH",
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export class HttpRequest {
|
|
12
|
+
protected method: HttpMethod;
|
|
13
|
+
protected url: string;
|
|
14
|
+
protected headers: Record<string, string>;
|
|
15
|
+
protected body?: any;
|
|
16
|
+
protected params: Record<string, string | number>;
|
|
17
|
+
protected requireAuth: boolean = true;
|
|
18
|
+
protected noCache: boolean = false;
|
|
19
|
+
constructor(
|
|
20
|
+
method: HttpMethod,
|
|
21
|
+
url: string,
|
|
22
|
+
headers?: Record<string, string>,
|
|
23
|
+
body?: any,
|
|
24
|
+
params?: Record<string, string | number>,
|
|
25
|
+
noCache: boolean = false,
|
|
26
|
+
) {
|
|
27
|
+
this.method = method;
|
|
28
|
+
this.url = url;
|
|
29
|
+
this.headers = headers || {};
|
|
30
|
+
this.body = body;
|
|
31
|
+
this.params = params || {};
|
|
32
|
+
this.noCache = noCache;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
getMethod(): HttpMethod {
|
|
36
|
+
return this.method;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
getUrl(): string {
|
|
40
|
+
return this.url;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
getHeaders(): Record<string, string> {
|
|
44
|
+
return this.headers;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
getBody(): any {
|
|
48
|
+
return this.body;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
getParams(): Record<string, string | number> {
|
|
52
|
+
const urlObj = new URL(this.url, "http://dummybase");
|
|
53
|
+
urlObj.searchParams.forEach((value, key) => {
|
|
54
|
+
if (!(key in this.params)) {
|
|
55
|
+
this.params[key] = value;
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
return this.params;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
setHeader(key: string, value: string): void {
|
|
62
|
+
this.headers[key] = value;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
addCookie(name: string, value: string): void {
|
|
66
|
+
const cookieString = `${name}=${value}`;
|
|
67
|
+
if (this.headers["Cookie"]) {
|
|
68
|
+
this.headers["Cookie"] += `; ${cookieString}`;
|
|
69
|
+
} else {
|
|
70
|
+
this.headers["Cookie"] = cookieString;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
mergeHeaders(headers: Record<string, string>): void {
|
|
75
|
+
this.headers = { ...this.headers, ...headers };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
mergeParams(params: Record<string, string | number>): void {
|
|
79
|
+
this.params = { ...this.params, ...params };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
setUrl(url: string): void {
|
|
83
|
+
this.url = url;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
setHost(host: string): void {
|
|
87
|
+
if (this.url.startsWith("http://") || this.url.startsWith("https://")) {
|
|
88
|
+
const urlObj = new URL(this.url);
|
|
89
|
+
urlObj.host = host;
|
|
90
|
+
this.url = urlObj.toString();
|
|
91
|
+
} else {
|
|
92
|
+
this.url = `${host}${this.url}`;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
turnOffAuth(): void {
|
|
97
|
+
this.requireAuth = false;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
isAuthRequired(): boolean {
|
|
101
|
+
return this.requireAuth;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
turnOnAuth(): void {
|
|
105
|
+
this.requireAuth = true;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
isNoCache(): boolean {
|
|
109
|
+
return this.noCache;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
enableCache(): void {
|
|
113
|
+
this.noCache = false;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
disableCache(): void {
|
|
117
|
+
this.noCache = true;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export abstract class BaseRequest<T> {
|
|
122
|
+
abstract buildRequest(): HttpRequest;
|
|
123
|
+
abstract getParser(): IParser<T>;
|
|
124
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./parser.interface";
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import * as cheerio from "cheerio";
|
|
2
|
+
|
|
3
|
+
export class HtmlDetect {
|
|
4
|
+
static isLoginPage(html: string): boolean {
|
|
5
|
+
const $ = HtmlDetect.load(html);
|
|
6
|
+
const loginBtn = $("button#logIn");
|
|
7
|
+
return loginBtn.length > 0;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
static load(html: string): cheerio.CheerioAPI {
|
|
11
|
+
return cheerio.load(html);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./html-detect";
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
export enum AccountHost {
|
|
2
|
+
DAOTAO = "https://daotao.vku.udn.vn",
|
|
3
|
+
DK1 = "https://dk1.vku.udn.vn",
|
|
4
|
+
DK2 = "https://dk2.vku.udn.vn",
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export class Account {
|
|
8
|
+
private host: AccountHost;
|
|
9
|
+
private cookie: string;
|
|
10
|
+
constructor(cookie: string);
|
|
11
|
+
constructor(cookie: string, host: AccountHost);
|
|
12
|
+
constructor(cookie: string, host?: AccountHost) {
|
|
13
|
+
this.cookie = cookie;
|
|
14
|
+
if (host) {
|
|
15
|
+
this.host = host;
|
|
16
|
+
} else {
|
|
17
|
+
this.host = AccountHost.DAOTAO;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
getHost(): AccountHost {
|
|
22
|
+
return this.host;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
useDaoTaoHost(): Account {
|
|
26
|
+
this.host = AccountHost.DAOTAO;
|
|
27
|
+
return this;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
useDK1Host(): Account {
|
|
31
|
+
this.host = AccountHost.DK1;
|
|
32
|
+
return this;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
useDK2Host(): Account {
|
|
36
|
+
this.host = AccountHost.DK2;
|
|
37
|
+
return this;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
getCookie(): string {
|
|
41
|
+
if (this.cookie.includes("=")) return this.cookie;
|
|
42
|
+
|
|
43
|
+
return `laravel_session=${this.cookie}`;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./account.entity";
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { ISessionProvider } from "../../../core/auth/session-provider.interface";
|
|
2
|
+
import { Account } from "../domain/account.entity";
|
|
3
|
+
|
|
4
|
+
export class AccountSessionProvider implements ISessionProvider {
|
|
5
|
+
constructor(private account: Account) {}
|
|
6
|
+
getAuthHeader(): Promise<Record<string, string>> {
|
|
7
|
+
return Promise.resolve({
|
|
8
|
+
Cookie: this.account.getCookie(),
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
getHost(): string {
|
|
12
|
+
return this.account.getHost();
|
|
13
|
+
}
|
|
14
|
+
isAuthenticated(): boolean {
|
|
15
|
+
return this.account.getCookie().length > 0;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { ISessionProvider } from "../../../core";
|
|
2
|
+
import { Account, AccountHost } from "../domain";
|
|
3
|
+
import { AccountSessionProvider } from "./account-session.provider";
|
|
4
|
+
import { ChromeCdpLogin } from "./chrome-cdp-login";
|
|
5
|
+
|
|
6
|
+
export interface GoogleAccountData {
|
|
7
|
+
email: string;
|
|
8
|
+
password: string;
|
|
9
|
+
host?: AccountHost;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface SessionAccountData {
|
|
13
|
+
session: string;
|
|
14
|
+
host?: AccountHost;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export class AccountFactory {
|
|
18
|
+
static createAccount(
|
|
19
|
+
data: GoogleAccountData | SessionAccountData,
|
|
20
|
+
): ISessionProvider {
|
|
21
|
+
if (isGoogleAccountData(data)) {
|
|
22
|
+
return new ChromeCdpLogin(
|
|
23
|
+
data.email,
|
|
24
|
+
data.password,
|
|
25
|
+
data.host || AccountHost.DAOTAO,
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
if (isSessionAccountData(data)) {
|
|
29
|
+
return new AccountSessionProvider(
|
|
30
|
+
new Account(data.session, data.host || AccountHost.DAOTAO),
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
throw new Error("Invalid account data");
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function isGoogleAccountData(
|
|
39
|
+
data: GoogleAccountData | SessionAccountData,
|
|
40
|
+
): data is GoogleAccountData {
|
|
41
|
+
return (data as GoogleAccountData).email !== undefined;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function isSessionAccountData(
|
|
45
|
+
data: GoogleAccountData | SessionAccountData,
|
|
46
|
+
): data is SessionAccountData {
|
|
47
|
+
return (data as SessionAccountData).session !== undefined;
|
|
48
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { ISessionProvider } from "../../../core";
|
|
2
|
+
export class ChromeCdpLogin implements ISessionProvider {
|
|
3
|
+
private cookie: string = "";
|
|
4
|
+
constructor(
|
|
5
|
+
private readonly email: string,
|
|
6
|
+
private readonly password: string,
|
|
7
|
+
private readonly host: string,
|
|
8
|
+
private readonly userDataDir?: string,
|
|
9
|
+
) {
|
|
10
|
+
throw new Error("Not implemented yet! Please create account with session cookie");
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
async getAuthHeader(): Promise<Record<string, string>> {
|
|
14
|
+
console.log("[ChromeCdpLogin] start");
|
|
15
|
+
if (this.cookie) {
|
|
16
|
+
return {
|
|
17
|
+
Cookie: this.cookie,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return {
|
|
22
|
+
Cookie: await this.getCookie(),
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
getHost(): string {
|
|
27
|
+
return this.host;
|
|
28
|
+
}
|
|
29
|
+
isAuthenticated(): boolean {
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
private async getCookie(): Promise<string> {
|
|
34
|
+
const { ChromeWrapper } = await import("../../../modules/shared/infra/chrome-wrapper.js");
|
|
35
|
+
const chrome = await this.launchChrome();
|
|
36
|
+
const browser = new ChromeWrapper();
|
|
37
|
+
await browser.connect({ port: chrome.port });
|
|
38
|
+
try {
|
|
39
|
+
await browser.navigate(this.host + "/sv");
|
|
40
|
+
await browser.click("#logIn");
|
|
41
|
+
await browser.wait(3000);
|
|
42
|
+
} catch {
|
|
43
|
+
throw new Error("Login failed");
|
|
44
|
+
} finally {
|
|
45
|
+
await browser.disconnect();
|
|
46
|
+
chrome.kill();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return "";
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
private async launchChrome() {
|
|
53
|
+
const ChromeLauncher = await import("chrome-launcher");
|
|
54
|
+
const chrome = await ChromeLauncher.launch({
|
|
55
|
+
chromeFlags: [
|
|
56
|
+
"--user-data-dir=" + (this.userDataDir || "./chrome-user-data"),
|
|
57
|
+
],
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
return chrome;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export class ClassSession {
|
|
2
|
+
constructor(
|
|
3
|
+
public readonly subjectClassName: string,
|
|
4
|
+
public readonly teacher: string,
|
|
5
|
+
public readonly weeksRaw: string,
|
|
6
|
+
public readonly room: string,
|
|
7
|
+
public readonly dayOfWeek: string,
|
|
8
|
+
public readonly period: string,
|
|
9
|
+
public readonly credit?: number,
|
|
10
|
+
public readonly timeSlot?: string,
|
|
11
|
+
public readonly id?: number,
|
|
12
|
+
) {}
|
|
13
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export class CourseSection {
|
|
2
|
+
constructor(
|
|
3
|
+
public readonly id: number,
|
|
4
|
+
public readonly courseId: number,
|
|
5
|
+
public readonly maxCapacity: number,
|
|
6
|
+
) {}
|
|
7
|
+
|
|
8
|
+
isValid(): boolean {
|
|
9
|
+
return this.id !== -1 && this.courseId !== -1;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
parseToRegistrationPayload() {
|
|
13
|
+
return {
|
|
14
|
+
hocphan_id: this.id,
|
|
15
|
+
idlop: this.courseId,
|
|
16
|
+
m: this.maxCapacity,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { CourseSection } from "./course-section.entity";
|
|
2
|
+
|
|
3
|
+
export enum SubjectClassStatus {
|
|
4
|
+
CAN_REGISTER = "CAN_REGISTER",
|
|
5
|
+
FULL = "FULL",
|
|
6
|
+
TIME_CONFLICT = "TIME_CONFLICT",
|
|
7
|
+
NOT_ELIGIBLE = "NOT_ELIGIBLE",
|
|
8
|
+
REGISTRATION_CLOSED = "REGISTRATION_CLOSED",
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export class SubjectClass {
|
|
12
|
+
constructor(
|
|
13
|
+
public readonly id: string,
|
|
14
|
+
public readonly subjectName: string,
|
|
15
|
+
|
|
16
|
+
public readonly teacher: string,
|
|
17
|
+
public readonly timeTable: string,
|
|
18
|
+
public readonly weeks: string,
|
|
19
|
+
|
|
20
|
+
public readonly capacity: number,
|
|
21
|
+
public readonly registeredCount: number,
|
|
22
|
+
|
|
23
|
+
public readonly enrollmentParams: CourseSection | null,
|
|
24
|
+
|
|
25
|
+
public readonly statusMessage: SubjectClassStatus,
|
|
26
|
+
) {
|
|
27
|
+
if (this.isFull()) this.statusMessage = SubjectClassStatus.FULL;
|
|
28
|
+
if (this.getAvailableSlots() === 0)
|
|
29
|
+
this.statusMessage = SubjectClassStatus.FULL;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
public isOpenForRegistration(): boolean {
|
|
33
|
+
return this.enrollmentParams !== null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
public isFull(): boolean {
|
|
37
|
+
return this.registeredCount >= this.capacity;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
public getAvailableSlots(): number {
|
|
41
|
+
return Math.max(0, this.capacity - this.registeredCount);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
public getEnrollmentParamsOrThrow(): CourseSection {
|
|
45
|
+
if (!this.enrollmentParams) {
|
|
46
|
+
throw new Error(
|
|
47
|
+
`Cannot register for class ${this.id}. Reason: ${this.statusMessage}`,
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
return this.enrollmentParams;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
clone(overrides: Partial<SubjectClass> = {}) {
|
|
54
|
+
return new SubjectClass(
|
|
55
|
+
overrides.id ?? this.id,
|
|
56
|
+
overrides.subjectName ?? this.subjectName,
|
|
57
|
+
overrides.teacher ?? this.teacher,
|
|
58
|
+
overrides.timeTable ?? this.timeTable,
|
|
59
|
+
overrides.weeks ?? this.weeks,
|
|
60
|
+
overrides.capacity ?? this.capacity,
|
|
61
|
+
overrides.registeredCount ?? this.registeredCount,
|
|
62
|
+
overrides.enrollmentParams ?? this.enrollmentParams,
|
|
63
|
+
overrides.statusMessage ?? this.statusMessage,
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { HtmlDetect, IParser } from "../../../core";
|
|
2
|
+
import { HtmlTableParser } from "../../shared";
|
|
3
|
+
import { ClassSession } from "../domain";
|
|
4
|
+
|
|
5
|
+
export class ClassSessionParser implements IParser<ClassSession[]> {
|
|
6
|
+
private htmlTableParser = new HtmlTableParser();
|
|
7
|
+
parse(html: string): ClassSession[] {
|
|
8
|
+
const $ = HtmlDetect.load(html);
|
|
9
|
+
if (html.length === 0) {
|
|
10
|
+
return []
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
let table = $(".table-responsive.today table")
|
|
14
|
+
|
|
15
|
+
if (table.length > 0) {
|
|
16
|
+
return this.parseFromSchedule($.html(table));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
table = $("body > div > div > div.right_col > div > div > div > div > div > div.x_content > div > table.hientai.table.table-striped.jambo_table.bulk_action")
|
|
20
|
+
|
|
21
|
+
return this.parseFromScheduleV2($.html(table));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
private parseFromScheduleV2(raw: string): ClassSession[] {
|
|
25
|
+
const result: ClassSession[] = [];
|
|
26
|
+
|
|
27
|
+
const data = this.htmlTableParser.parse(raw);
|
|
28
|
+
for (const item of data) {
|
|
29
|
+
if (!item["Thời khóa biểu"]) continue;
|
|
30
|
+
const [dayOfWeek, period] = item["Thời khóa biểu"].split(" ")
|
|
31
|
+
|
|
32
|
+
const $ = HtmlDetect.load(item.rawHTML)
|
|
33
|
+
const link = $("a").attr("href") || ""
|
|
34
|
+
const id = this.extractClassIdFromLink(link) ?? undefined
|
|
35
|
+
|
|
36
|
+
result.push(
|
|
37
|
+
new ClassSession(
|
|
38
|
+
item["Tên học phần"],
|
|
39
|
+
item["Giảng viên"],
|
|
40
|
+
item["Tuần học"],
|
|
41
|
+
"",
|
|
42
|
+
dayOfWeek,
|
|
43
|
+
period,
|
|
44
|
+
Number(item["Số TC"]),
|
|
45
|
+
item["Thời gian ĐK"],
|
|
46
|
+
id,
|
|
47
|
+
)
|
|
48
|
+
)
|
|
49
|
+
}
|
|
50
|
+
return result
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
private extractClassIdFromLink(link: string): number | null {
|
|
54
|
+
const urlParams = new URLSearchParams(link.split("?")[1]);
|
|
55
|
+
const idParam = urlParams.get("id");
|
|
56
|
+
return idParam ? Number(idParam) : null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
private parseFromSchedule(raw: string): ClassSession[] {
|
|
60
|
+
const result: ClassSession[] = [];
|
|
61
|
+
|
|
62
|
+
const data = this.htmlTableParser.parse(raw);
|
|
63
|
+
|
|
64
|
+
for (const item of data) {
|
|
65
|
+
const classSession = new ClassSession(
|
|
66
|
+
item["Tên lớp học phần"],
|
|
67
|
+
item["Giảng viên"],
|
|
68
|
+
item["Tuần"],
|
|
69
|
+
item["Phòng"],
|
|
70
|
+
item["Thứ"],
|
|
71
|
+
item["Tiết"],
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
result.push(classSession);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return result;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { IParser } from "../../../core";
|
|
2
|
+
import {
|
|
3
|
+
HtmlTableParser,
|
|
4
|
+
TableCellWithRawHTML,
|
|
5
|
+
} from "../../shared/parsers/html-table.parser";
|
|
6
|
+
import { CourseSection } from "../domain";
|
|
7
|
+
import {
|
|
8
|
+
SubjectClass,
|
|
9
|
+
SubjectClassStatus,
|
|
10
|
+
} from "../domain/subject-class.entity";
|
|
11
|
+
import * as cheerio from "cheerio";
|
|
12
|
+
export class SubjectClassParser implements IParser<SubjectClass[]> {
|
|
13
|
+
private tableParser: HtmlTableParser = new HtmlTableParser();
|
|
14
|
+
parse(html: string): SubjectClass[] {
|
|
15
|
+
const $ = cheerio.load(html);
|
|
16
|
+
const data = this.tableParser.parse($.html()) as TableCellWithRawHTML[];
|
|
17
|
+
const result: SubjectClass[] = [];
|
|
18
|
+
for (const item of data) {
|
|
19
|
+
if (item["STT"] === "") continue;
|
|
20
|
+
const rawHTML = item.rawHTML;
|
|
21
|
+
const courseSection = this.parseCourseSections(rawHTML);
|
|
22
|
+
const status = this.parseStatus(item["Tùy chọn"] || "");
|
|
23
|
+
const subjectClass = new SubjectClass(
|
|
24
|
+
courseSection?.id.toString() || "",
|
|
25
|
+
item["Tên học phần"],
|
|
26
|
+
item["Giảng viên"],
|
|
27
|
+
item["Thời khóa biểu"],
|
|
28
|
+
item["Tuần học"],
|
|
29
|
+
parseInt(item["Sỉ số"], 10),
|
|
30
|
+
parseInt(item["Số lượng đăng ký"], 10),
|
|
31
|
+
courseSection,
|
|
32
|
+
status,
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
result.push(subjectClass);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return result;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
private parseCourseSections(raw: string): CourseSection | null {
|
|
42
|
+
const $ = cheerio.load(raw);
|
|
43
|
+
const link = $("a");
|
|
44
|
+
const el = link.first();
|
|
45
|
+
const href = el.attr("href") || "";
|
|
46
|
+
const regex = /hocphan_id=(\d+)&idlop=(\d+)&m=(\d+)/;
|
|
47
|
+
const match = href.match(regex);
|
|
48
|
+
if (match) {
|
|
49
|
+
const courseSection = new CourseSection(
|
|
50
|
+
parseInt(match[1], 10),
|
|
51
|
+
parseInt(match[2], 10),
|
|
52
|
+
parseInt(match[3], 10),
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
return courseSection;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
private parseStatus(message: string): SubjectClassStatus {
|
|
62
|
+
message = message.toLowerCase();
|
|
63
|
+
if (message.includes("chọn")) return SubjectClassStatus.CAN_REGISTER;
|
|
64
|
+
if (message.includes("đầy")) return SubjectClassStatus.FULL;
|
|
65
|
+
if (message.includes("trùng")) return SubjectClassStatus.TIME_CONFLICT;
|
|
66
|
+
if (message.includes("không đủ điều kiện"))
|
|
67
|
+
return SubjectClassStatus.NOT_ELIGIBLE;
|
|
68
|
+
if (message.includes("đóng đăng ký"))
|
|
69
|
+
return SubjectClassStatus.REGISTRATION_CLOSED;
|
|
70
|
+
return SubjectClassStatus.NOT_ELIGIBLE;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { BaseRequest, HttpMethod, HttpRequest, IParser } from "../../../core";
|
|
2
|
+
import { ClassSession } from "../domain";
|
|
3
|
+
import { ClassSessionParser } from "../parsers/class-session.parser";
|
|
4
|
+
|
|
5
|
+
export class GetRegisteredSubjectClassRequest extends BaseRequest<
|
|
6
|
+
ClassSession[]
|
|
7
|
+
> {
|
|
8
|
+
buildRequest(): HttpRequest {
|
|
9
|
+
return new HttpRequest(HttpMethod.GET, "/sv/dang-ky-tin-chi");
|
|
10
|
+
}
|
|
11
|
+
getParser(): IParser<ClassSession[]> {
|
|
12
|
+
return new ClassSessionParser();
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { BaseRequest, HttpMethod, HttpRequest, IParser } from "../../../core";
|
|
2
|
+
import { SubjectClass } from "../domain/subject-class.entity";
|
|
3
|
+
import { SubjectClassParser } from "../parsers/subject-class.parser";
|
|
4
|
+
|
|
5
|
+
export class GetSubjectClassesRequest extends BaseRequest<SubjectClass[]> {
|
|
6
|
+
constructor(public readonly courseId: number) {
|
|
7
|
+
super();
|
|
8
|
+
}
|
|
9
|
+
buildRequest(): HttpRequest {
|
|
10
|
+
// return {
|
|
11
|
+
// method: "GET",
|
|
12
|
+
// url: `/sv/tin-chi-xem-chi-tiet?id=${this.courseId}`,
|
|
13
|
+
// };
|
|
14
|
+
//
|
|
15
|
+
return new HttpRequest(
|
|
16
|
+
HttpMethod.GET,
|
|
17
|
+
`/sv/tin-chi-xem-chi-tiet`,
|
|
18
|
+
{},
|
|
19
|
+
undefined,
|
|
20
|
+
{
|
|
21
|
+
id: this.courseId,
|
|
22
|
+
},
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
getParser(): IParser<SubjectClass[]> {
|
|
26
|
+
return new SubjectClassParser();
|
|
27
|
+
}
|
|
28
|
+
}
|