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

SKEDGE-14 #comment convert to async/await instead of .then() logics, organizing awaits #30

5 changes: 3 additions & 2 deletions src/data/config.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { RMPRatingInterface } from "~data/interfaces";
export const HEADERS = {
"Authorization": "Basic dGVzdDp0ZXN0",
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36",
Expand All @@ -18,6 +19,7 @@ export const NEBULA_FETCH_OPTIONS = {
}

export const SCHOOL_ID = "1273"
export const RMP_GRAPHQL_URL = "https://www.ratemyprofessors.com/graphql";

function unRegister(key: string) {
let newVar = "";
Expand All @@ -27,5 +29,4 @@ function unRegister(key: string) {
newVar = newVar.concat(String.fromCharCode(a));
}
return newVar;
}

}
129 changes: 89 additions & 40 deletions src/data/fetchFromRmp.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import { HEADERS, PROFESSOR_QUERY } from "~data/config";
import { HEADERS, PROFESSOR_QUERY, RMP_GRAPHQL_URL } from "~data/config";
import type { RMPRatingInterface } from "~data/interfaces";

function reportError(context, err) {
console.error("Error in " + context + ": " + err);
}

function getProfessorUrl(professorName: string, schoolId: string): string {
return `https://www.ratemyprofessors.com/search/teachers?query=${encodeURIComponent(professorName)}&sid=${btoa(`School-${schoolId}`)}`
Expand All @@ -15,16 +20,24 @@ function getProfessorIds(texts: string[], professorNames: string[]): string[] {
const professorIds = []
const lowerCaseProfessorNames = professorNames.map(name => name.toLowerCase())
texts.forEach(text => {
let matched = false;
const regex = /"legacyId":(\d+).*?"firstName":"(.*?)","lastName":"(.*?)"/g;
for (const match of text.matchAll(regex)) {
console.log(match[2].split(' ')[0].toLowerCase() + " " + match[3].toLowerCase())
if (lowerCaseProfessorNames.includes(match[2].split(' ')[0].toLowerCase() + " " + match[3].toLowerCase())) {
professorIds.push(match[1]);
matched = true;
let pendingMatch = null;
const regex = /"legacyId":(\d+).*?"numRatings":(\d+).*?"firstName":"(.*?)","lastName":"(.*?)"/g;
let allMatches: string[] = text.match(regex);
let 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() + " ")
let numRatings = parseInt(match[2]);
if (lowerCaseProfessorNames.includes(match[3].split(' ')[0].toLowerCase() + " " + match[4].toLowerCase()) && numRatings >= highestNumRatings) {
pendingMatch = match[1];
}
}
}
}
if (!matched) professorIds.push(null)

professorIds.push(pendingMatch);
})
return professorIds
}
Expand All @@ -46,48 +59,84 @@ function getGraphQlUrlProps(professorIds: string[]) {
return graphQlUrlProps
}

function fetchWithGraphQl(graphQlUrlProps: any[], resolve) {
const graphqlUrl = "https://www.ratemyprofessors.com/graphql";
function wait(delay){
return new Promise((resolve) => setTimeout(resolve, delay));
}

Promise.all(graphQlUrlProps.map(u=>fetch(graphqlUrl, u)))
.then(responses => Promise.all(responses.map(res => res.json())))
.then(ratings => {
for (let i = 0; i < ratings.length; i++) {
if (ratings[i] != null && ratings[i].hasOwnProperty("data") && ratings[i]["data"].hasOwnProperty("node")) {
ratings[i] = ratings[i]["data"]["node"];
}
function fetchRetry(url: string, delay: number, tries: number, fetchOptions) {
function onError(err){
let triesLeft: number = tries - 1;
if(!triesLeft){
throw err;
}
return wait(delay).then(() => fetchRetry(url, delay, triesLeft, fetchOptions));
}
return fetch(url,fetchOptions).catch(onError);
}

// If using orderedFetchOpts, make sure that it is an array and that the index of the fetch options corresponds to the index of the response in the responses array.
async function validateResponses(responses: any[], orderedFetchOpts: any[]) {
for (const [key, value] of Object.entries(responses)) {
let notOk = value?.status !== 200;
if (notOk && value && value.url) {
let details = {
status: value.status,
statusText: value.statusText,
redirected: value.redirected,
url: value.url
}
console.log(ratings)
resolve(ratings)
})
reportError("validateResponses", "Status not OK for fetch request. Details are: "+JSON.stringify(details));
let fetchOptions = orderedFetchOpts[key] || {}; // If we don't have fetch options, we just use an empty object.
responses[key] = await fetchRetry(value?.url, 200, 3, fetchOptions);
}
};
return responses;
}

export async function fetchWithGraphQl(graphQlUrlProps: any[]) {
try {
let responses = await validateResponses(await Promise.all(graphQlUrlProps.map(u=>fetch(RMP_GRAPHQL_URL, u))),graphQlUrlProps);
// We now have all the responses. So, we consider all the responses, and collect the ratings.
let ratings: RMPRatingInterface [] = await Promise.all(responses.map(res => res.json()));
for (let i = 0; i < ratings.length; i++) {
if (ratings[i] != null && ratings[i].hasOwnProperty("data") && ratings[i]["data"].hasOwnProperty("node")) {
ratings[i] = ratings[i]["data"]["node"];
}
}
return ratings;
}
catch (err) {
reportError("fetchWithGraphQl",err);
return [];
}
}

export interface RmpRequest {
professorNames: string[],
schoolId: string
}
export function requestProfessorsFromRmp(request: RmpRequest): Promise<RMPInterface[]> {
return new Promise((resolve, reject) => {

// make a list of urls for promises
const professorUrls = getProfessorUrls(request.professorNames, request.schoolId)
export async function requestProfessorsFromRmp(request: RmpRequest): Promise<RMPInterface[]> {

// fetch professor ids from each url
Promise.all(professorUrls.map(u=>fetch(u)))
.then(responses => Promise.all(responses.map(res => res.text())))
.then(texts => {
const professorIds = getProfessorIds(texts, request.professorNames)
// make a list of urls for promises
const professorUrls = getProfessorUrls(request.professorNames, request.schoolId)

// fetch professor ids from each url
try {
let responses = await validateResponses(await Promise.all(professorUrls.map(u=>(fetch(u)))), []);

let texts = await Promise.all(responses.map(res => res.text()));
const professorIds = getProfessorIds(texts, request.professorNames)

// create fetch objects for each professor id
const graphQlUrlProps = getGraphQlUrlProps(professorIds)
// create fetch objects for each professor id
const graphQlUrlProps = getGraphQlUrlProps(professorIds)

// fetch professor info by id with graphQL
fetchWithGraphQl(graphQlUrlProps, resolve)
}
).catch(error => {
reject(error);
});
})
// fetch professor info by id with graphQL
let professors = await fetchWithGraphQl(graphQlUrlProps);
return professors;
} catch (error) {
reportError("requestProfessorsFromRmp", error);
return [];
};
}

interface RMPInterface {
Expand Down
35 changes: 34 additions & 1 deletion src/data/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,4 +85,37 @@ export interface SectionInterface {
section_number: string;
syllabus_uri: string;
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

}