Skip to content

Commit 0762214

Browse files
h34312575saddison
and
addison
authored
DISC-119-unielectives-review (#188)
* working unilectives review * added warning for chartjs dependency * changed to ephemeral reply * removed unneccesary libraries for chartjs * hopefully no npm package conflict dying * npm packages should be fixed * changed web scraper package for robustness * linting * fixed yaml npm issue * JSCode docu * linting * revert config files to og * package-json changes --------- Co-authored-by: addison <[email protected]>
1 parent 8e1acf4 commit 0762214

File tree

4 files changed

+2089
-386
lines changed

4 files changed

+2089
-386
lines changed

commands/courseRating.js

+161
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
const { SlashCommandBuilder } = require("@discordjs/builders");
2+
const { EmbedBuilder, AttachmentBuilder } = require("discord.js");
3+
const { ChartJSNodeCanvas } = require("chartjs-node-canvas");
4+
const puppeteer = require("puppeteer");
5+
6+
/**
7+
* Extracts the relevant information from the course page
8+
*/
9+
async function extractRating(url) {
10+
const browser = await puppeteer.launch();
11+
12+
const page = await browser.newPage();
13+
await page.goto(url, { waitUntil: "networkidle2" });
14+
15+
const courseTitle = await page.$eval(
16+
"h2.text-3xl.font-bold.break-words",
17+
(el) => el.textContent,
18+
);
19+
const numReviews = await page.$eval(".space-x-2 > span", (el) => el.textContent);
20+
const ratings = await page.$$eval(".flex.flex-wrap.justify-around > div", (items) => {
21+
const result = [];
22+
items.slice(0, 3).forEach((el) => {
23+
const rating = el.querySelector(".text-2xl.font-bold").textContent;
24+
const category = el.querySelector(".text-center.font-bold").textContent;
25+
result.push({
26+
name: category,
27+
value: `${rating} out of 5`,
28+
inline: true,
29+
});
30+
});
31+
return result;
32+
});
33+
34+
const fullDescription = await page.$eval(".whitespace-pre-line", (el) => el.textContent);
35+
const description = fullDescription.split(/(?<=[.!?])\s/)[0].trim();
36+
37+
await browser.close();
38+
return { courseTitle, numReviews, description, ratings };
39+
}
40+
41+
/**
42+
* Determines the color code based on the given rating.
43+
*
44+
* @param {number} rating - The rating value to evaluate.
45+
* @returns {string} - The corresponding color code in hexadecimal format.
46+
*
47+
*/
48+
function ratingColour(rating) {
49+
if (rating >= 3.5) {
50+
return "#39e75f";
51+
} else if (rating > 2.5) {
52+
return "#FFA500";
53+
}
54+
return "#FF0000";
55+
}
56+
57+
/**
58+
* Builds a doughnut chart representing the average rating from a list of ratings.
59+
*
60+
* @param {Array} ratings - An array of rating objects
61+
* @returns {Promise<Buffer>} - An image buffer of doughnut chart
62+
*/
63+
async function buildChart(ratings) {
64+
const width = 800;
65+
const height = 300;
66+
const averageRating =
67+
ratings.reduce((sum, rating) => {
68+
return sum + parseFloat(rating.value.split(" ")[0]);
69+
}, 0) / ratings.length;
70+
71+
const canvas = new ChartJSNodeCanvas({ width, height });
72+
73+
const config = {
74+
type: "doughnut",
75+
data: {
76+
datasets: [
77+
{
78+
data: [averageRating, 5 - averageRating],
79+
backgroundColor: [ratingColour(averageRating), "#e0e0e0"],
80+
borderJoinStyle: "round",
81+
borderRadius: [
82+
{
83+
outerStart: 20,
84+
innerStart: 20,
85+
},
86+
{
87+
outerEnd: 20,
88+
innerEnd: 20,
89+
},
90+
],
91+
borderWidth: 0,
92+
},
93+
],
94+
},
95+
options: {
96+
rotation: 290,
97+
circumference: 140,
98+
cutout: "88%",
99+
plugins: {
100+
legend: {
101+
display: false,
102+
},
103+
},
104+
},
105+
};
106+
107+
const image = await canvas.renderToBuffer(config);
108+
return image;
109+
}
110+
111+
module.exports = {
112+
data: new SlashCommandBuilder()
113+
.setName("courserating")
114+
.setDescription("Tells you the current rating of a specific course!")
115+
.addStringOption((option) =>
116+
option.setName("course").setDescription("Enter the course code").setRequired(true),
117+
),
118+
async execute(interaction) {
119+
const course = interaction.options.getString("course");
120+
121+
const url = `https://unilectives.devsoc.app/course/${course}`;
122+
123+
const year = new Date().getFullYear();
124+
const handbookUrl = `https://www.handbook.unsw.edu.au/undergraduate/courses/${year}/${course}`;
125+
126+
try {
127+
await interaction.deferReply({ ephemeral: true });
128+
129+
const { courseTitle, numReviews, description, ratings } = await extractRating(url);
130+
131+
if (numReviews == "0 reviews") {
132+
await interaction.editReply({
133+
content: "Sorry there are no reviews for this course yet 😔",
134+
});
135+
return;
136+
}
137+
138+
const image = await buildChart(ratings);
139+
const attachment = new AttachmentBuilder(image, { name: "rating.png" });
140+
ratings.unshift({
141+
name: "\u200B",
142+
value: `[${course} Handbook](${handbookUrl})`,
143+
});
144+
const replyEmbed = new EmbedBuilder()
145+
.setColor(0x0099ff)
146+
.setTitle(course + " " + courseTitle)
147+
.setURL(url)
148+
.setDescription(description)
149+
.setImage("attachment://rating.png")
150+
.addFields(ratings)
151+
.setFooter({ text: numReviews });
152+
153+
await interaction.editReply({ embeds: [replyEmbed], files: [attachment] });
154+
} catch (err) {
155+
console.log(err);
156+
await interaction.editReply({
157+
content: `Sorry the course could not be found! 😔`,
158+
});
159+
}
160+
},
161+
};

config/wordle.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
{
22
"players": []
3-
}
3+
}

0 commit comments

Comments
 (0)