Skip to content

Commit

Permalink
feat: favorite
Browse files Browse the repository at this point in the history
  • Loading branch information
AsakuraMizu committed Jul 4, 2024
1 parent cf2d68b commit 60197ae
Show file tree
Hide file tree
Showing 4 changed files with 173 additions and 9 deletions.
104 changes: 104 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
CredentialProvider,
Discussion,
FailReason,
FavoriteItem,
Fetch,
File,
FileCategory,
Expand All @@ -36,6 +37,7 @@ import {
} from './types';
import * as URLS from './urls';
import {
FAVORITE_TYPE_MAP_REVERSE,
GRADE_LEVEL_MAP,
JSONP_EXTRACTOR_NAME,
decodeHTML,
Expand Down Expand Up @@ -717,6 +719,108 @@ export class Learn2018Helper {
return questions;
}

/**
* Add an item to favorites. (收藏)
*/
public async addToFavorites(type: ContentType, id: string): Promise<void> {
const json = await (await this.#myFetchWithToken(URLS.LEARN_FAVORITE_ADD(type, id))).json();
if (json.result !== 'success') {
return Promise.reject({
reason: FailReason.INVALID_RESPONSE,
extra: json,
} as ApiError);
}
}

/**
* Remove an item from favorites. (取消收藏)
*/
public async removeFromFavorites(id: string): Promise<void> {
const json = await (await this.#myFetchWithToken(URLS.LEARN_FAVORITE_REMOVE(id))).json();
if (json.result !== 'success') {
return Promise.reject({
reason: FailReason.INVALID_RESPONSE,
extra: json,
} as ApiError);
}
}

/**
* Get favorites. (我的收藏)
* If `courseID` or `type` is specified, only return favorites of that course or type.
*/
public async getFavorites(courseID?: string, type?: ContentType): Promise<FavoriteItem[]> {
const json = await (
await this.#myFetchWithToken(URLS.LEARN_FAVORITE_LIST(type), {
method: 'POST',
body: URLS.LEARN_FAVORITE_LIST_FORM_DATA(courseID),
})
).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((e): FavoriteItem | undefined => {
const type = FAVORITE_TYPE_MAP_REVERSE.get(e.ywlx);
if (!type) return; // ignore unknown type
return {
id: e.ywid,
type,
title: decodeHTML(e.ywbt),
time: type === ContentType.DISCUSSION || type === ContentType.QUESTION ? new Date(e.tlsj) : new Date(e.ywsj),
state: e.ywzt,
extra: e.ywbz ?? undefined,
semesterId: e.xnxq,
courseId: e.wlkcid,
pinned: e.sfzd === '是',
pinnedTime: e.zdsj === null ? undefined : new Date(e.zdsj), // Note: this field is originally unix timestamp instead of string
addedTime: new Date(e.scsj),
itemId: e.id,
} satisfies FavoriteItem;
})
.filter((x) => !!x);
}

/**
* Pin a favorite item. (置顶)
*/
public async pinFavoriteItem(id: string): Promise<void> {
const json = await (
await this.#myFetchWithToken(URLS.LEARN_FAVORITE_PIN, {
method: 'POST',
body: URLS.LEARN_FAVORITE_PIN_UNPIN_FORM_DATA(id),
})
).json();
if (json.result !== 'success') {
return Promise.reject({
reason: FailReason.INVALID_RESPONSE,
extra: json,
} as ApiError);
}
}

/**
* Unpin a favorite item. (取消置顶)
*/
public async unpinFavoriteItem(id: string): Promise<void> {
const json = await (
await this.#myFetchWithToken(URLS.LEARN_FAVORITE_UNPIN, {
method: 'POST',
body: URLS.LEARN_FAVORITE_PIN_UNPIN_FORM_DATA(id),
})
).json();
if (json.result !== 'success') {
return Promise.reject({
reason: FailReason.INVALID_RESPONSE,
extra: json,
} as ApiError);
}
}

private async getHomeworkListAtUrl(url: string, status: IHomeworkStatus): Promise<Homework[]> {
const json = await (await this.#myFetchWithToken(url)).json();
if (json.result !== 'success') {
Expand Down
20 changes: 20 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,26 @@ interface ICourseContent<T extends ContentType> {

export type CourseContent<T extends ContentType = ContentType> = ICourseContent<T>;

interface IFavoriteItem {
id: string;
type: ContentType;
title: string;
time: Date;
state: string;
/** extra message. For homework, this will be deadline (plus score if graded). It's too flexible and hard to parse so we leave it as is. */
extra?: string;
semesterId: string;
courseId: string;
pinned: boolean;
pinnedTime?: Date;
// comment?: string;
addedTime: Date;
/** for reference */
itemId: string;
}

export type FavoriteItem = IFavoriteItem;

export interface CalendarEvent {
location: string;
status: string;
Expand Down
26 changes: 25 additions & 1 deletion src/urls.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { FormData } from 'node-fetch-native';
import { ContentType, CourseType, IHomeworkSubmitAttachment, Language } from './types';
import { getMkFromType } from './utils';
import { FAVORITE_TYPE_MAP, getMkFromType } from './utils';

export const LEARN_PREFIX = 'https://learn.tsinghua.edu.cn';
export const REGISTRAR_PREFIX = 'https://zhjw.cic.tsinghua.edu.cn';
Expand Down Expand Up @@ -188,6 +188,30 @@ export const WebsiteShowLanguage = {
export const LEARN_WEBSITE_LANGUAGE = (lang: Language) =>
`https://learn.tsinghua.edu.cn/f/wlxt/common/language?websiteShowLanguage=${WebsiteShowLanguage[lang]}`;

export const LEARN_FAVORITE_ADD = (type: ContentType, id: string) =>
`${LEARN_PREFIX}/b/xt/wlkc_xsscb/student/add?ywid=${id}&ywlx=${FAVORITE_TYPE_MAP.get(type)}`;

export const LEARN_FAVORITE_REMOVE = (id: string) => `${LEARN_PREFIX}/b/xt/wlkc_xsscb/student/delete?ywid=${id}`;

export const LEARN_FAVORITE_LIST = (type?: ContentType) =>
`${LEARN_PREFIX}/b/xt/wlkc_xsscb/student/pageList?ywlx=${type ? FAVORITE_TYPE_MAP.get(type) : 'ALL'}`;

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

export const LEARN_FAVORITE_PIN = `${LEARN_PREFIX}/b/xt/wlkc_xsscb/student/addZd`;

export const LEARN_FAVORITE_UNPIN = `${LEARN_PREFIX}/b/xt/wlkc_xsscb/student/delZd`;

export const LEARN_FAVORITE_PIN_UNPIN_FORM_DATA = (id: string) => {
const form = new FormData();
form.append('ywid', id);
return form;
};

export const REGISTRAR_TICKET_FORM_DATA = () => {
const form = new FormData();
form.append('appId', 'ALL_ZHJW');
Expand Down
32 changes: 24 additions & 8 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,16 @@ export function parseSemesterType(n: number): SemesterType {
}
}

const CONTENT_TYPE_MK_MAP = new Map([
[ContentType.NOTIFICATION, 'kcgg'],
[ContentType.FILE, 'kcwj'],
[ContentType.HOMEWORK, 'kczy'],
[ContentType.DISCUSSION, ''],
[ContentType.QUESTION, ''],
]);
const CONTENT_TYPE_MK_MAP = {
[ContentType.NOTIFICATION]: 'kcgg',
[ContentType.FILE]: 'kcwj',
[ContentType.HOMEWORK]: 'kczy',
[ContentType.DISCUSSION]: '',
[ContentType.QUESTION]: '',
};

export function getMkFromType(type: ContentType): string {
return 'mk_' + (CONTENT_TYPE_MK_MAP.get(type) ?? 'UNKNOWN');
return 'mk_' + (CONTENT_TYPE_MK_MAP[type] ?? 'UNKNOWN');
}

export function decodeHTML(html: string): string {
Expand Down Expand Up @@ -92,3 +92,19 @@ export function formatFileSize(size: number): string {
if (size < 1024 * 1024 * 1024) return (size / 1024 / 1024).toFixed(2) + 'M';
return (size / 1024 / 1024 / 1024).toFixed(2) + 'G';
}

export const FAVORITE_TYPE_MAP = new Map([
[ContentType.NOTIFICATION, 'KCGG'],
[ContentType.FILE, 'KCKJ'],
[ContentType.HOMEWORK, 'KCZY'],
[ContentType.DISCUSSION, 'KCTL'],
[ContentType.QUESTION, 'KCDY'],
// omitted: 问卷(KCWJ) & 课表(KCKB) as they are not supported now
]);
export const FAVORITE_TYPE_MAP_REVERSE = new Map([
[FAVORITE_TYPE_MAP.get(ContentType.NOTIFICATION)!, ContentType.NOTIFICATION],
[FAVORITE_TYPE_MAP.get(ContentType.FILE)!, ContentType.FILE],
[FAVORITE_TYPE_MAP.get(ContentType.HOMEWORK)!, ContentType.HOMEWORK],
[FAVORITE_TYPE_MAP.get(ContentType.DISCUSSION)!, ContentType.DISCUSSION],
[FAVORITE_TYPE_MAP.get(ContentType.QUESTION)!, ContentType.QUESTION],
]);

0 comments on commit 60197ae

Please sign in to comment.