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.
Files changed (57) hide show
  1. package/README.md +38 -0
  2. package/package.json +36 -0
  3. package/pnpm-workspace.yaml +2 -0
  4. package/src/VkuSDK.ts +184 -0
  5. package/src/core/auth/index.ts +1 -0
  6. package/src/core/auth/session-provider.interface.ts +5 -0
  7. package/src/core/cache/cache.interface.ts +6 -0
  8. package/src/core/cache/index.ts +2 -0
  9. package/src/core/cache/infra/file-cache.ts +82 -0
  10. package/src/core/cache/infra/index.ts +3 -0
  11. package/src/core/cache/infra/memory-cache.ts +32 -0
  12. package/src/core/cache/infra/storage-cache.ts +24 -0
  13. package/src/core/executor/index.ts +1 -0
  14. package/src/core/executor/request-executor.ts +20 -0
  15. package/src/core/http/adapters/axios-http-client.ts +25 -0
  16. package/src/core/http/adapters/http-account.decorator.ts +19 -0
  17. package/src/core/http/adapters/http-cache.decorator.ts +55 -0
  18. package/src/core/http/adapters/http-logger.decorator.ts +29 -0
  19. package/src/core/http/adapters/http-retry.decorator.ts +34 -0
  20. package/src/core/http/adapters/index.ts +5 -0
  21. package/src/core/http/base-request.ts +124 -0
  22. package/src/core/http/http-client.interface.ts +5 -0
  23. package/src/core/http/index.ts +3 -0
  24. package/src/core/index.ts +6 -0
  25. package/src/core/parser/index.ts +1 -0
  26. package/src/core/parser/parser.interface.ts +3 -0
  27. package/src/core/utils/html-detect.ts +13 -0
  28. package/src/core/utils/index.ts +1 -0
  29. package/src/index.ts +4 -0
  30. package/src/modules/accounts/domain/account.entity.ts +45 -0
  31. package/src/modules/accounts/domain/index.ts +1 -0
  32. package/src/modules/accounts/index.ts +2 -0
  33. package/src/modules/accounts/infra/account-session.provider.ts +17 -0
  34. package/src/modules/accounts/infra/account.factory.ts +48 -0
  35. package/src/modules/accounts/infra/chrome-cdp-login.ts +62 -0
  36. package/src/modules/accounts/infra/index.ts +3 -0
  37. package/src/modules/enrollment/domain/class-session.entity.ts +13 -0
  38. package/src/modules/enrollment/domain/course-section.entity.ts +19 -0
  39. package/src/modules/enrollment/domain/index.ts +4 -0
  40. package/src/modules/enrollment/domain/subject-action-response.entity.ts +6 -0
  41. package/src/modules/enrollment/domain/subject-class.entity.ts +66 -0
  42. package/src/modules/enrollment/index.ts +3 -0
  43. package/src/modules/enrollment/parsers/class-session.parser.ts +79 -0
  44. package/src/modules/enrollment/parsers/index.ts +2 -0
  45. package/src/modules/enrollment/parsers/subject-class.parser.ts +72 -0
  46. package/src/modules/enrollment/requests/get-registered-subject-class.request.ts +14 -0
  47. package/src/modules/enrollment/requests/get-subject-classes.request.ts +28 -0
  48. package/src/modules/enrollment/requests/index.ts +4 -0
  49. package/src/modules/enrollment/requests/register-subject-class.request.ts +21 -0
  50. package/src/modules/enrollment/requests/unregister-subject-class.request.ts +24 -0
  51. package/src/modules/index.ts +3 -0
  52. package/src/modules/shared/index.ts +2 -0
  53. package/src/modules/shared/infra/chrome-wrapper.ts +380 -0
  54. package/src/modules/shared/infra/index.ts +1 -0
  55. package/src/modules/shared/parsers/html-table.parser.ts +320 -0
  56. package/src/modules/shared/parsers/index.ts +1 -0
  57. package/tsconfig.json +21 -0
@@ -0,0 +1,4 @@
1
+ export * from "./get-subject-classes.request";
2
+ export * from "./register-subject-class.request";
3
+ export * from "./unregister-subject-class.request";
4
+ export * from "./get-registered-subject-class.request";
@@ -0,0 +1,21 @@
1
+ import { BaseRequest, HttpMethod, HttpRequest, IParser } from "../../../core";
2
+ import { ClassSession, CourseSection, SubjectActionResponse } from "../domain";
3
+ import { ClassSessionParser } from "../parsers";
4
+
5
+ export class RegisterSubjectClassRequest extends BaseRequest<ClassSession[]> {
6
+ constructor(private courseSection: CourseSection) {
7
+ super();
8
+ }
9
+ buildRequest(): HttpRequest {
10
+ return new HttpRequest(
11
+ HttpMethod.GET,
12
+ "/sv/dang-ky-tung-lophp",
13
+ {},
14
+ undefined,
15
+ this.courseSection.parseToRegistrationPayload(),
16
+ );
17
+ }
18
+ getParser(): IParser<ClassSession[]> {
19
+ return new ClassSessionParser();
20
+ }
21
+ }
@@ -0,0 +1,24 @@
1
+ import { BaseRequest, HttpMethod, HttpRequest, IParser } from "../../../core";
2
+ import { ClassSession } from "../domain";
3
+ import { ClassSessionParser } from "../parsers";
4
+
5
+ export class UnregisterSubjectClassRequest extends BaseRequest<ClassSession[]> {
6
+ constructor(public readonly classRegistrationId: number) {
7
+ super();
8
+ }
9
+ buildRequest(): HttpRequest {
10
+ return new HttpRequest(
11
+ HttpMethod.GET,
12
+ `/sv/xoa-dang-ky-tung-lophp`,
13
+ {},
14
+ undefined,
15
+ {
16
+ id: this.classRegistrationId,
17
+ },
18
+ );
19
+ }
20
+
21
+ getParser(): IParser<ClassSession[]> {
22
+ return new ClassSessionParser();
23
+ }
24
+ }
@@ -0,0 +1,3 @@
1
+ export * from "./accounts";
2
+ export * from "./enrollment";
3
+ export * from "./shared";
@@ -0,0 +1,2 @@
1
+ export * from "./parsers";
2
+ export * from "./infra";
@@ -0,0 +1,380 @@
1
+ import CDP from "chrome-remote-interface";
2
+
3
+ interface NavigationOptions {
4
+ timeout?: number;
5
+ waitUntil?: "load" | "domcontentloaded" | "networkidle";
6
+ }
7
+
8
+ interface WaitForSelectorOptions {
9
+ timeout?: number;
10
+ visible?: boolean;
11
+ }
12
+
13
+ export class ChromeWrapper {
14
+ private client: CDP.Client | null = null;
15
+ private navigationPromise: Promise<void> | null = null;
16
+
17
+ async connect(options?: CDP.Options): Promise<void> {
18
+ this.client = await CDP(options);
19
+ const { Page, Runtime, DOM, Network } = this.client;
20
+
21
+ await Promise.all([
22
+ Page.enable(),
23
+ Runtime.enable(),
24
+ DOM.enable(),
25
+ Network.enable(),
26
+ ]);
27
+ }
28
+
29
+ async disconnect(): Promise<void> {
30
+ if (this.client) {
31
+ await this.client.close();
32
+ this.client = null;
33
+ }
34
+ }
35
+
36
+ async navigate(url: string, options: NavigationOptions = {}): Promise<void> {
37
+ if (!this.client) throw new Error("Not connected");
38
+
39
+ const { timeout = 30000, waitUntil = "load" } = options;
40
+ const { Page } = this.client;
41
+
42
+ try {
43
+ this.navigationPromise = this.waitForNavigation(waitUntil, timeout);
44
+
45
+ await Page.navigate({ url });
46
+
47
+ await this.navigationPromise;
48
+ } catch (error) {
49
+ throw new Error(`Navigation to ${url} failed: ${error}`);
50
+ } finally {
51
+ this.navigationPromise = null;
52
+ }
53
+ }
54
+
55
+ private waitForNavigation(waitUntil: string, timeout: number): Promise<void> {
56
+ if (!this.client) throw new Error("Not connected");
57
+
58
+ const { Page, Network } = this.client;
59
+
60
+ return new Promise((resolve, reject) => {
61
+ const timeoutId = setTimeout(() => {
62
+ reject(new Error("Navigation timeout"));
63
+ }, timeout);
64
+
65
+ const cleanup = () => {
66
+ clearTimeout(timeoutId);
67
+ };
68
+
69
+ if (waitUntil === "load") {
70
+ Page.loadEventFired(() => {
71
+ cleanup();
72
+ resolve();
73
+ });
74
+ } else if (waitUntil === "domcontentloaded") {
75
+ Page.domContentEventFired(() => {
76
+ cleanup();
77
+ resolve();
78
+ });
79
+ } else if (waitUntil === "networkidle") {
80
+ let idleTimeout: NodeJS.Timeout;
81
+ let requestCount = 0;
82
+
83
+ const checkIdle = () => {
84
+ if (requestCount === 0) {
85
+ clearTimeout(idleTimeout);
86
+ idleTimeout = setTimeout(() => {
87
+ cleanup();
88
+ resolve();
89
+ }, 500);
90
+ }
91
+ };
92
+
93
+ Network.requestWillBeSent(() => {
94
+ requestCount++;
95
+ });
96
+
97
+ Network.loadingFinished(() => {
98
+ requestCount--;
99
+ checkIdle();
100
+ });
101
+
102
+ Network.loadingFailed(() => {
103
+ requestCount--;
104
+ checkIdle();
105
+ });
106
+ }
107
+ });
108
+ }
109
+
110
+ async waitForSelector(
111
+ selector: string,
112
+ options: WaitForSelectorOptions = {},
113
+ ): Promise<number> {
114
+ if (!this.client) throw new Error("Not connected");
115
+
116
+ const { timeout = 30000, visible = true } = options;
117
+ const { Runtime, DOM } = this.client;
118
+
119
+ const startTime = Date.now();
120
+
121
+ while (Date.now() - startTime < timeout) {
122
+ try {
123
+ const result = await Runtime.evaluate({
124
+ expression: `document.querySelector('${selector}')`,
125
+ returnByValue: false,
126
+ });
127
+
128
+ if (result.result.objectId) {
129
+ const { node } = await DOM.describeNode({
130
+ objectId: result.result.objectId,
131
+ });
132
+
133
+ if (visible) {
134
+ const isVisible = await this.isElementVisible(
135
+ result.result.objectId,
136
+ );
137
+ if (isVisible) {
138
+ return node.backendNodeId;
139
+ }
140
+ } else {
141
+ return node.backendNodeId;
142
+ }
143
+ }
144
+ } catch (error) {
145
+ // Ignore errors and retry
146
+ }
147
+
148
+ await this.sleep(100);
149
+ }
150
+
151
+ throw new Error(`Timeout waiting for selector: ${selector}`);
152
+ }
153
+
154
+ /**
155
+ * Kiểm tra element có visible không
156
+ */
157
+ private async isElementVisible(objectId: string): Promise<boolean> {
158
+ if (!this.client) throw new Error("Not connected");
159
+
160
+ const { Runtime } = this.client;
161
+
162
+ const result = await Runtime.callFunctionOn({
163
+ objectId,
164
+ functionDeclaration: `function() {
165
+ const rect = this.getBoundingClientRect();
166
+ const style = window.getComputedStyle(this);
167
+ return rect.width > 0 &&
168
+ rect.height > 0 &&
169
+ style.visibility !== 'hidden' &&
170
+ style.display !== 'none';
171
+ }`,
172
+ returnByValue: true,
173
+ });
174
+
175
+ return result.result.value === true;
176
+ }
177
+
178
+ async click(
179
+ selector: string,
180
+ options: WaitForSelectorOptions = {},
181
+ ): Promise<void> {
182
+ if (!this.client) throw new Error("Not connected");
183
+
184
+ await this.waitForSelector(selector, options);
185
+
186
+ const { Runtime } = this.client;
187
+
188
+ const result = await Runtime.evaluate({
189
+ expression: `
190
+ (function() {
191
+ const element = document.querySelector('${selector}');
192
+ if (!element) throw new Error('Element not found');
193
+ element.click();
194
+ return true;
195
+ })()
196
+ `,
197
+ returnByValue: true,
198
+ });
199
+
200
+ if (result.exceptionDetails) {
201
+ throw new Error(
202
+ `Click failed ${result.exceptionDetails.exception?.description}`,
203
+ );
204
+ }
205
+ }
206
+
207
+ async fill(
208
+ selector: string,
209
+ text: string,
210
+ options: WaitForSelectorOptions = {},
211
+ ): Promise<void> {
212
+ if (!this.client) throw new Error("Not connected");
213
+
214
+ await this.waitForSelector(selector, options);
215
+
216
+ const { Runtime } = this.client;
217
+
218
+ await Runtime.evaluate({
219
+ expression: `
220
+ (function() {
221
+ const element = document.querySelector('${selector}');
222
+ if (!element) throw new Error('Element not found');
223
+ element.value = '';
224
+ element.dispatchEvent(new Event('input', { bubbles: true }));
225
+ })()
226
+ `,
227
+ });
228
+
229
+ const escapedText = text.replace(/'/g, "\\'").replace(/\n/g, "\\n");
230
+
231
+ const result = await Runtime.evaluate({
232
+ expression: `
233
+ (function() {
234
+ const element = document.querySelector('${selector}');
235
+ if (!element) throw new Error('Element not found');
236
+ element.value = '${escapedText}';
237
+ element.dispatchEvent(new Event('input', { bubbles: true }));
238
+ element.dispatchEvent(new Event('change', { bubbles: true }));
239
+ return true;
240
+ })()
241
+ `,
242
+ returnByValue: true,
243
+ });
244
+
245
+ if (result.exceptionDetails) {
246
+ throw new Error(
247
+ `Fill failed: ${result.exceptionDetails.exception?.description}`,
248
+ );
249
+ }
250
+ }
251
+
252
+ async getText(
253
+ selector: string,
254
+ options: WaitForSelectorOptions = {},
255
+ ): Promise<string> {
256
+ if (!this.client) throw new Error("Not connected");
257
+
258
+ await this.waitForSelector(selector, options);
259
+
260
+ const { Runtime } = this.client;
261
+
262
+ const result = await Runtime.evaluate({
263
+ expression: `
264
+ (function() {
265
+ const element = document.querySelector('${selector}');
266
+ if (!element) throw new Error('Element not found');
267
+ return element.textContent || element.innerText || '';
268
+ })()
269
+ `,
270
+ returnByValue: true,
271
+ });
272
+
273
+ if (result.exceptionDetails) {
274
+ throw new Error(
275
+ `Get text failed: ${result.exceptionDetails.exception?.description}`,
276
+ );
277
+ }
278
+
279
+ return result.result.value as string;
280
+ }
281
+
282
+ async getAttribute(
283
+ selector: string,
284
+ attribute: string,
285
+ options: WaitForSelectorOptions = {},
286
+ ): Promise<string | null> {
287
+ if (!this.client) throw new Error("Not connected");
288
+
289
+ await this.waitForSelector(selector, options);
290
+
291
+ const { Runtime } = this.client;
292
+
293
+ const result = await Runtime.evaluate({
294
+ expression: `
295
+ (function() {
296
+ const element = document.querySelector('${selector}');
297
+ if (!element) throw new Error('Element not found');
298
+ return element.getAttribute('${attribute}');
299
+ })()
300
+ `,
301
+ returnByValue: true,
302
+ });
303
+
304
+ if (result.exceptionDetails) {
305
+ throw new Error(
306
+ `Get attribute failed: ${result.exceptionDetails.exception?.description}`,
307
+ );
308
+ }
309
+
310
+ return result.result.value as string | null;
311
+ }
312
+
313
+ async screenshot(
314
+ options: { path?: string; fullPage?: boolean } = {},
315
+ ): Promise<string> {
316
+ if (!this.client) throw new Error("Not connected");
317
+
318
+ const { Page } = this.client;
319
+ const { fullPage = false } = options;
320
+
321
+ const screenshot = await Page.captureScreenshot({
322
+ format: "png",
323
+ captureBeyondViewport: fullPage,
324
+ });
325
+
326
+ return screenshot.data;
327
+ }
328
+
329
+ async evaluate<T>(
330
+ fn: string | ((...args: any[]) => T),
331
+ ...args: any[]
332
+ ): Promise<T> {
333
+ if (!this.client) throw new Error("Not connected");
334
+
335
+ const { Runtime } = this.client;
336
+
337
+ const expression =
338
+ typeof fn === "function"
339
+ ? `(${fn.toString()})(${args.map((arg) => JSON.stringify(arg)).join(",")})`
340
+ : fn;
341
+
342
+ const result = await Runtime.evaluate({
343
+ expression,
344
+ returnByValue: true,
345
+ awaitPromise: true,
346
+ });
347
+
348
+ if (result.exceptionDetails) {
349
+ throw new Error(
350
+ `Evaluate failed: ${result.exceptionDetails.exception?.description}`,
351
+ );
352
+ }
353
+
354
+ return result.result.value as T;
355
+ }
356
+
357
+ private sleep(ms: number): Promise<void> {
358
+ return new Promise((resolve) => setTimeout(resolve, ms));
359
+ }
360
+
361
+ async wait(ms: number): Promise<void> {
362
+ await this.sleep(ms);
363
+ }
364
+ async reload(options: NavigationOptions = {}): Promise<void> {
365
+ if (!this.client) throw new Error("Not connected");
366
+
367
+ const { timeout = 30000, waitUntil = "load" } = options;
368
+ const { Page } = this.client;
369
+
370
+ try {
371
+ this.navigationPromise = this.waitForNavigation(waitUntil, timeout);
372
+ await Page.reload();
373
+ await this.navigationPromise;
374
+ } catch (error) {
375
+ throw new Error(`Reload failed: ${error}`);
376
+ } finally {
377
+ this.navigationPromise = null;
378
+ }
379
+ }
380
+ }
@@ -0,0 +1 @@
1
+ export * from "./chrome-wrapper";