diff --git a/src/data/config.ts b/src/data/config.ts index d9a6d98..38ee566 100644 --- a/src/data/config.ts +++ b/src/data/config.ts @@ -12,3 +12,4 @@ export const PROFESSOR_QUERY = { }; export const SCHOOL_ID = '1273'; +export const RMP_GRAPHQL_URL = 'https://www.ratemyprofessors.com/graphql'; diff --git a/src/data/fetchFromRmp.ts b/src/data/fetchFromRmp.ts index 27f79bb..455cf9b 100644 --- a/src/data/fetchFromRmp.ts +++ b/src/data/fetchFromRmp.ts @@ -1,4 +1,8 @@ -import { HEADERS, PROFESSOR_QUERY } from '~data/config'; +import { HEADERS, PROFESSOR_QUERY, RMP_GRAPHQL_URL } from '~data/config'; + +function reportError(context, err) { + console.error('Error in ' + context + ': ' + err); +} function getProfessorUrl(professorName: string, schoolId: string): string { const url = new URL( @@ -9,24 +13,37 @@ function getProfessorUrl(professorName: string, schoolId: string): string { } function getProfessorId(text: string, professorName: string): string { - let professorId = ''; const lowerCaseProfessorName = professorName.toLowerCase(); - let matched = false; - const regex = /"legacyId":(\d+).*?"firstName":"(.*?)","lastName":"(.*?)"/g; - for (const match of text.matchAll(regex)) { - if ( - lowerCaseProfessorName.includes( - match[2].split(' ')[0].toLowerCase() + ' ' + match[3].toLowerCase(), - ) - ) { - professorId = match[1]; - matched = true; + let pendingMatch = null; + const regex = + /"legacyId":(\d+).*?"numRatings":(\d+).*?"firstName":"(.*?)","lastName":"(.*?)"/g; + const allMatches: string[] = text.match(regex); + const highestNumRatings = 0; + + if (allMatches) { + for (const fullMatch of allMatches) { + for (const match of fullMatch.matchAll(regex)) { + console.log( + match[3].split(' ')[0].toLowerCase() + + ' ' + + match[4].toLowerCase() + + ' ', + ); + const numRatings = parseInt(match[2]); + if ( + lowerCaseProfessorName.includes( + match[3].split(' ')[0].toLowerCase() + ' ' + match[4].toLowerCase(), + ) && + numRatings >= highestNumRatings + ) { + pendingMatch = match[1]; + } + } } } - if (!matched) professorId = null; - return professorId; + return pendingMatch; } function getGraphQlUrlProp(professorId: string) { @@ -41,21 +58,63 @@ function getGraphQlUrlProp(professorId: string) { }; } +function wait(delay) { + return new Promise((resolve) => setTimeout(resolve, delay)); +} + +function fetchRetry(url: string, delay: number, tries: number, fetchOptions) { + function onError(err) { + const triesLeft: number = tries - 1; + if (!triesLeft) { + throw err; + } + return wait(delay).then(() => + fetchRetry(url, delay, triesLeft, fetchOptions), + ); + } + return fetch(url, fetchOptions).catch(onError); +} + +async function validateResponse(response, fetchOptions) { + const notOk = response?.status !== 200; + if (notOk && response && response.url) { + const details = { + status: response.status, + statusText: response.statusText, + redirected: response.redirected, + url: response.url, + }; + reportError( + 'validateResponses', + 'Status not OK for fetch request. Details are: ' + + JSON.stringify(details), + ); + // If we don't have fetch options, we just use an empty object. + responses = await fetchRetry(response?.url, 200, 3, fetchOptions || {}); + } + return response; +} + function fetchWithGraphQl(graphQlUrlProp, resolve) { - const graphqlUrl = 'https://www.ratemyprofessors.com/graphql'; - - fetch(graphqlUrl, graphQlUrlProp) - .then((response) => response.json()) - .then((rating) => { - if ( - rating != null && - Object.hasOwn(rating, 'data') && - Object.hasOwn(rating['data'], 'node') - ) { - rating = rating['data']['node']; - } - resolve(rating); - }); + try { + fetch(RMP_GRAPHQL_URL, graphQlUrlProp).then((response) => + validateResponse(response, graphQlUrlProp) + .then((response) => response.json()) + .then((rating) => { + if ( + rating != null && + Object.hasOwn(rating, 'data') && + Object.hasOwn(rating['data'], 'node') + ) { + rating = rating['data']['node']; + } + resolve(rating); + }), + ); + } catch (err) { + reportError('fetchWithGraphQl', err); + resolve(null); /// + } } export interface RmpRequest { @@ -85,6 +144,7 @@ export function requestProfessorFromRmp( fetchWithGraphQl(graphQlUrlProp, resolve); }) .catch((error) => { + reportError('requestProfessorFromRmp', error); reject(error); }); }); diff --git a/src/data/interfaces.ts b/src/data/interfaces.ts new file mode 100644 index 0000000..518bb3a --- /dev/null +++ b/src/data/interfaces.ts @@ -0,0 +1,129 @@ +//TODO: Fix these any types + +export interface FetchProfessorParameters { + firstName: string; + lastName: string; +} + +export interface FetchCourseParameters { + subjectPrefix: string; + courseNumber: string; +} + +export interface FetchSectionParameters { + courseReference: string; + professorReference: string; +} + +interface Requisites { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + options: any[]; + required: number; + type: string; +} + +export interface CourseInterface { + __v: number; + _id: string; + activity_type: string; + class_level: string; + co_or_pre_requisites: Requisites; + corequisites: Requisites; + course_number: string; + credit_hours: string; + description: string; + grading: string; + internal_course_number: string; + laboratory_contact_hours: string; + lecture_contact_hours: string; + offering_frequency: string; + prerequisites: Requisites; + school: string; + sections: string[]; + subject_prefix: string; + title: string; +} + +interface Office { + building: string; + room: string; + map_uri: string; +} + +export interface ProfessorInterface { + __v: number; + _id: string; + email: string; + first_name: string; + image_uri: string; + last_name: string; + office: Office; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + office_hours: any[]; + phone_number: string; + profile_uri: string; + sections: string[]; + titles: string[]; +} + +export interface SectionInterface { + __v: number; + _id: string; + academic_session: { + end_date: string; + name: string; + start_date: string; + }; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + attributes: any[]; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + core_flags: any[]; + course_reference: string; + grade_distribution: number[]; + instruction_mode: string; + internal_class_number: string; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + meetings: any[]; + professors: string[]; + section_corequisites: { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + options: any[]; + type: string; + }; + section_number: string; + syllabus_uri: string; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + teaching_assistants: any[]; +} + +export interface CourseCodeInterface { + courseCount: number; + courseName: string; +} +export interface RatingsDistributionInterface { + r1: number; + r2: number; + r3: number; + r4: number; + r5: number; + total: number; +} +export interface TeacherRatingTag { + tagCount: number; + tagName: string; +} + +export interface RMPRatingInterface { + avgDifficulty: number; + avgRating: number; + courseCodes: CourseCodeInterface[]; + department: string; + firstName: string; + lastName: string; + legacyId: number; + numRatings: number; + ratingsDistribution: RatingsDistributionInterface; + school: { id: string }; + teacherRatingTags: TeacherRatingTag[]; + wouldTakeAgainPercent: number; +}