Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

支持文件分类相关 API #58

Merged
merged 7 commits into from
May 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,10 @@ It's ok if you meet `Timeout * Async callback was not invoked within the 5000ms

## Changelog

- (unreleased)
- Get file categories
- Get file list by category
- Fix file download url and visit count in TA
- v3.1.4
- Allow and check for undefined credential in login (see [#52](https://github.com/Harry-Chen/Learn-Helper/issues/52))
- Discriminate `getAllContents` return type based on input content type (see [#53](https://github.com/Harry-Chen/Learn-Helper/issues/53))
Expand Down
200 changes: 155 additions & 45 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import {
JSONP_EXTRACTOR_NAME,
decodeHTML,
extractJSONPResult,
formatFileSize,
parseSemesterType,
trimAndDefine,
} from './utils';
Expand Down Expand Up @@ -115,13 +116,13 @@ export class Learn2018Helper {
this.#myFetch = this.#provider
? this.#withReAuth(this.#rawFetch)
: async (...args) => {
const result = await this.#rawFetch(...args);
if (noLogin(result))
return Promise.reject({
reason: FailReason.NOT_LOGGED_IN,
} as ApiError);
return result;
};
const result = await this.#rawFetch(...args);
if (noLogin(result))
return Promise.reject({
reason: FailReason.NOT_LOGGED_IN,
} as ApiError);
return result;
};
}

/** fetch CSRF token from helper (invalid after login / re-login), might be '' if not logged in */
Expand Down Expand Up @@ -431,61 +432,170 @@ export class Learn2018Helper {
// student
result = json.object;
}
const files: File[] = [];

await Promise.all(
result.map(async (f) => {
const categories = new Map((await this.getFileCategoryList(courseID, courseType)).map((c) => [c.id, c]));

return result.map((f) => {
const title = decodeHTML(f.bt);
const id = f.wjid;
const uploadTime = new Date(f.scsj);
const downloadUrl = URLS.LEARN_FILE_DOWNLOAD(id, courseType);
const previewUrl = URLS.LEARN_FILE_PREVIEW(ContentType.FILE, id, courseType, this.previewFirstPage);
return {
id,
id2: f.kjxxid,
category: categories.get(f.kjflid),
title,
description: decodeHTML(f.ms),
rawSize: f.wjdx,
size: f.fileSize,
uploadTime,
publishTime: uploadTime,
downloadUrl,
previewUrl,
isNew: f.isNew ?? false,
markedImportant: f.sfqd === 1,
visitCount: f.xsllcs ?? f.llcs ?? 0,
downloadCount: f.xzcs ?? 0,
fileType: f.wjlx,
remoteFile: {
id,
name: title,
downloadUrl,
previewUrl,
size: f.fileSize,
},
} satisfies File;
});
}

/** Get file categories of the specified course. */
public async getFileCategoryList(
courseID: string,
courseType: CourseType = CourseType.STUDENT,
): Promise<FileCategory[]> {
const json = await (await this.#myFetchWithToken(URLS.LEARN_FILE_CATEGORY_LIST(courseID, courseType))).json();
if (json.result !== 'success') {
return Promise.reject({
reason: FailReason.INVALID_RESPONSE,
extra: json,
} as ApiError);
}

const result = (json.object?.rows ?? []) as any[];

return result.map(
(c) =>
({
id: c.kjflid,
title: decodeHTML(c.bt),
creationTime: new Date(c.czsj),
}) satisfies FileCategory,
);
}

/**
* Get all files of the specified category of the specified course.
* Note: this cannot get correct `visitCount` and `downloadCount` for student
*/
public async getFileListByCategory(
courseID: string,
categoryId: string,
courseType: CourseType = CourseType.STUDENT,
): Promise<File[]> {
if (courseType === CourseType.STUDENT) {
const json = await (
await this.#myFetchWithToken(URLS.LEARN_FILE_LIST_BY_CATEGORY_STUDENT(courseID, categoryId))
).json();
if (json.result !== 'success') {
return Promise.reject({
reason: FailReason.INVALID_RESPONSE,
extra: json,
} as ApiError);
}

const result = (json.object ?? []) as any[];

return result.map((f) => {
const id = f[7];
const title = decodeHTML(f[1]);
const rawSize = f[9];
const size = formatFileSize(rawSize);
const downloadUrl = URLS.LEARN_FILE_DOWNLOAD(id, courseType);
const previewUrl = URLS.LEARN_FILE_PREVIEW(ContentType.FILE, id, courseType, this.previewFirstPage);
return {
id,
id2: f[0],
title,
description: decodeHTML(f[5]),
rawSize,
size,
uploadTime: new Date(f[6]),
publishTime: new Date(f[10]),
downloadUrl,
previewUrl,
isNew: f[8] === 1,
markedImportant: f[2] === 1,
visitCount: 0,
downloadCount: 0,
fileType: f[13],
remoteFile: {
id,
name: title,
downloadUrl,
previewUrl,
size,
},
} satisfies File;
});
} else {
const json = await (
await this.#myFetchWithToken(URLS.LEARN_FILE_LIST_BY_CATEGORY_TEACHER, {
method: 'POST',
body: URLS.LEARN_FILE_LIST_BY_CATEGORY_TEACHER_FORM_DATA(courseID, categoryId),
})
).json();
if (json.result !== 'success') {
return Promise.reject({
reason: FailReason.INVALID_RESPONSE,
extra: json,
} as ApiError);
}

const result = (json.object.aaData ?? []) as any[];

return result.map((f) => {
const title = decodeHTML(f.bt);
const downloadUrl = URLS.LEARN_FILE_DOWNLOAD(
courseType === CourseType.STUDENT ? f.wjid : f.id,
courseType,
courseID,
);
const previewUrl = URLS.LEARN_FILE_PREVIEW(ContentType.FILE, f.wjid, courseType, this.previewFirstPage);
files.push({
id: f.wjid,
title: decodeHTML(f.bt),
const id = f.wjid;
const uploadTime = new Date(f.scsj);
const downloadUrl = URLS.LEARN_FILE_DOWNLOAD(id, courseType);
const previewUrl = URLS.LEARN_FILE_PREVIEW(ContentType.FILE, id, courseType, this.previewFirstPage);
return {
id,
id2: f.kjxxid,
title,
description: decodeHTML(f.ms),
rawSize: f.wjdx,
size: f.fileSize,
uploadTime: new Date(f.scsj),
uploadTime,
publishTime: uploadTime,
downloadUrl,
previewUrl,
isNew: f.isNew,
isNew: f.isNew ?? false,
markedImportant: f.sfqd === 1,
visitCount: f.llcs ?? 0,
visitCount: f.xsllcs ?? f.llcs ?? 0,
downloadCount: f.xzcs ?? 0,
fileType: f.wjlx,
remoteFile: {
id: f.wjid,
id,
name: title,
downloadUrl,
previewUrl,
size: f.fileSize,
},
});
}),
);

return files;
}

public async getFileCategoryList(courseID: string, courseType: CourseType = CourseType.STUDENT): Promise<FileCategory[]> {
const json = await (await this.#myFetchWithToken(URLS.LEARN_FILE_CATEGORY_LIST(courseID, courseType))).json();
if (json.result !== 'success') {
return Promise.reject({
reason: FailReason.INVALID_RESPONSE,
extra: json,
} as ApiError);
} satisfies File;
});
}

const result = (json.object?.rows ?? []) as any[];

return result.map((c) => ({
id: c.kjflid,
title: decodeHTML(c.bt),
creationTime: new Date(c.czsj),
} satisfies FileCategory));
}

/** Get all homeworks (课程作业) of the specified course. */
Expand Down
21 changes: 12 additions & 9 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,16 +107,27 @@ export interface INotificationDetail {

export type Notification = INotification & INotificationDetail;

interface IFileCategory {
id: string;
title: string;
creationTime: Date;
}

export type FileCategory = IFileCategory;

interface IFile {
id: string;
id2: string;
/** note: will be unset when calling `getFileListByCategory` */
category?: FileCategory;
/** size in byte */
rawSize: number;
/** inaccurate size description (like '1M') */
size: string;
title: string;
description: string;
uploadTime: Date;
/** for teachers, this url will not initiate download directly */
publishTime: Date;
downloadUrl: string;
/** preview is not supported on all types of files, check before use */
previewUrl: string;
Expand All @@ -131,14 +142,6 @@ interface IFile {

export type File = IFile;

interface IFileCategory {
id: string;
title: string;
creationTime: Date;
}

export type FileCategory = IFileCategory;

export interface IHomeworkStatus {
submitted: boolean;
graded: boolean;
Expand Down
28 changes: 22 additions & 6 deletions src/urls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,12 +54,27 @@ export const LEARN_FILE_LIST = (courseID: string, courseType: CourseType) =>
: `${LEARN_PREFIX}/b/wlxt/kj/v_kjxxb_wjwjb/teacher/queryByWlkcid?wlkcid=${courseID}&size=${MAX_SIZE}`;

export const LEARN_FILE_CATEGORY_LIST = (courseID: string, courseType: CourseType) =>
`${LEARN_PREFIX}/b/wlxt/kj/wlkc_kjflb/${courseType}/pageList?wlkcid=${courseID}`
`${LEARN_PREFIX}/b/wlxt/kj/wlkc_kjflb/${courseType}/pageList?wlkcid=${courseID}`;

export const LEARN_FILE_DOWNLOAD = (fileID: string, courseType: CourseType, courseID: string) =>
courseType === CourseType.STUDENT
? `${LEARN_PREFIX}/b/wlxt/kj/wlkc_kjxxb/student/downloadFile?sfgk=0&wjid=${fileID}`
: `${LEARN_PREFIX}/f/wlxt/kj/wlkc_kjxxb/teacher/beforeView?id=${fileID}&wlkcid=${courseID}`;
export const LEARN_FILE_LIST_BY_CATEGORY_STUDENT = (courseID: string, categoryId: string) =>
`${LEARN_PREFIX}/b/wlxt/kj/wlkc_kjxxb/student/kjxxb/${courseID}/${categoryId}`;

export const LEARN_FILE_LIST_BY_CATEGORY_TEACHER = `${LEARN_PREFIX}/b/wlxt/kj/v_kjxxb_wjwjb/teacher/pageList`;

export const LEARN_FILE_LIST_BY_CATEGORY_TEACHER_FORM_DATA = (courseID: string, categoryId: string) => {
const form = new FormData();
form.append(
'aoData',
JSON.stringify([
{ name: 'wlkcid', value: courseID },
{ name: 'kjflid', value: categoryId },
]),
);
return form;
};

export const LEARN_FILE_DOWNLOAD = (fileID: string, courseType: CourseType) =>
`${LEARN_PREFIX}/b/wlxt/kj/wlkc_kjxxb/${courseType}/downloadFile?sfgk=0&wjid=${fileID}`;

export const LEARN_FILE_PREVIEW = (type: ContentType, fileID: string, courseType: CourseType, firstPageOnly = false) =>
`${LEARN_PREFIX}/f/wlxt/kc/wj_wjb/${courseType}/beforePlay?wjid=${fileID}&mk=${getMkFromType(
Expand Down Expand Up @@ -184,5 +199,6 @@ export const REGISTRAR_TICKET = () => `${LEARN_PREFIX}/b/wlxt/common/auth/gnt`;
export const REGISTRAR_AUTH = (ticket: string) => `${REGISTRAR_PREFIX}/j_acegi_login.do?url=/&ticket=${ticket}`;

export const REGISTRAR_CALENDAR = (startDate: string, endDate: string, graduate = false, callbackName = 'unknown') =>
`${REGISTRAR_PREFIX}/jxmh_out.do?m=${graduate ? 'yjs' : 'bks'
`${REGISTRAR_PREFIX}/jxmh_out.do?m=${
graduate ? 'yjs' : 'bks'
}_jxrl_all&p_start_date=${startDate}&p_end_date=${endDate}&jsoncallback=${callbackName}`;
8 changes: 8 additions & 0 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,3 +84,11 @@ export function extractJSONPResult(jsonp: string): any {
// evaluate the result
return Function(`"use strict";const ${JSONP_EXTRACTOR_NAME}=(s)=>s;return ${jsonp};`)();
}

export function formatFileSize(size: number): string {
// this logic is extracted from `judgeSize` function from Web Learning
if (size < 1024) return size + 'B';
if (size < 1024 * 1024) return (size / 1024).toFixed(2) + 'K';
if (size < 1024 * 1024 * 1024) return (size / 1024 / 1024).toFixed(2) + 'M';
return (size / 1024 / 1024 / 1024).toFixed(2) + 'G';
}
8 changes: 8 additions & 0 deletions test/data_retrival.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,14 @@ describe('helper data retrival', () => {
}
});

it('should get file categories and list correctly', async () => {
if (courseTester !== undefined) {
const categories = await helper.getFileCategoryList(courseTester);
expect(categories.length).toBeGreaterThanOrEqual(0);
expect((await helper.getFileListByCategory(courseTester, categories[0].id)).length).toBeGreaterThanOrEqual(0);
}
});

it('should get user info correctly', async () => {
const userInfo = await helper.getUserInfo();
expect(userInfo).toBeDefined();
Expand Down