Skip to content

Commit

Permalink
DISC-119-unielectives-review (#188)
Browse files Browse the repository at this point in the history
* 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 <h34312575s@gmail.com>
  • Loading branch information
h34312575s and addison authored Jun 16, 2024
1 parent 8e1acf4 commit 0762214
Show file tree
Hide file tree
Showing 4 changed files with 2,089 additions and 386 deletions.
161 changes: 161 additions & 0 deletions commands/courseRating.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
const { SlashCommandBuilder } = require("@discordjs/builders");
const { EmbedBuilder, AttachmentBuilder } = require("discord.js");
const { ChartJSNodeCanvas } = require("chartjs-node-canvas");
const puppeteer = require("puppeteer");

/**
* Extracts the relevant information from the course page
*/
async function extractRating(url) {
const browser = await puppeteer.launch();

const page = await browser.newPage();
await page.goto(url, { waitUntil: "networkidle2" });

const courseTitle = await page.$eval(
"h2.text-3xl.font-bold.break-words",
(el) => el.textContent,
);
const numReviews = await page.$eval(".space-x-2 > span", (el) => el.textContent);
const ratings = await page.$$eval(".flex.flex-wrap.justify-around > div", (items) => {
const result = [];
items.slice(0, 3).forEach((el) => {
const rating = el.querySelector(".text-2xl.font-bold").textContent;
const category = el.querySelector(".text-center.font-bold").textContent;
result.push({
name: category,
value: `${rating} out of 5`,
inline: true,
});
});
return result;
});

const fullDescription = await page.$eval(".whitespace-pre-line", (el) => el.textContent);
const description = fullDescription.split(/(?<=[.!?])\s/)[0].trim();

await browser.close();
return { courseTitle, numReviews, description, ratings };
}

/**
* Determines the color code based on the given rating.
*
* @param {number} rating - The rating value to evaluate.
* @returns {string} - The corresponding color code in hexadecimal format.
*
*/
function ratingColour(rating) {
if (rating >= 3.5) {
return "#39e75f";
} else if (rating > 2.5) {
return "#FFA500";
}
return "#FF0000";
}

/**
* Builds a doughnut chart representing the average rating from a list of ratings.
*
* @param {Array} ratings - An array of rating objects
* @returns {Promise<Buffer>} - An image buffer of doughnut chart
*/
async function buildChart(ratings) {
const width = 800;
const height = 300;
const averageRating =
ratings.reduce((sum, rating) => {
return sum + parseFloat(rating.value.split(" ")[0]);
}, 0) / ratings.length;

const canvas = new ChartJSNodeCanvas({ width, height });

const config = {
type: "doughnut",
data: {
datasets: [
{
data: [averageRating, 5 - averageRating],
backgroundColor: [ratingColour(averageRating), "#e0e0e0"],
borderJoinStyle: "round",
borderRadius: [
{
outerStart: 20,
innerStart: 20,
},
{
outerEnd: 20,
innerEnd: 20,
},
],
borderWidth: 0,
},
],
},
options: {
rotation: 290,
circumference: 140,
cutout: "88%",
plugins: {
legend: {
display: false,
},
},
},
};

const image = await canvas.renderToBuffer(config);
return image;
}

module.exports = {
data: new SlashCommandBuilder()
.setName("courserating")
.setDescription("Tells you the current rating of a specific course!")
.addStringOption((option) =>
option.setName("course").setDescription("Enter the course code").setRequired(true),
),
async execute(interaction) {
const course = interaction.options.getString("course");

const url = `https://unilectives.devsoc.app/course/${course}`;

const year = new Date().getFullYear();
const handbookUrl = `https://www.handbook.unsw.edu.au/undergraduate/courses/${year}/${course}`;

try {
await interaction.deferReply({ ephemeral: true });

const { courseTitle, numReviews, description, ratings } = await extractRating(url);

if (numReviews == "0 reviews") {
await interaction.editReply({
content: "Sorry there are no reviews for this course yet 😔",
});
return;
}

const image = await buildChart(ratings);
const attachment = new AttachmentBuilder(image, { name: "rating.png" });
ratings.unshift({
name: "\u200B",
value: `[${course} Handbook](${handbookUrl})`,
});
const replyEmbed = new EmbedBuilder()
.setColor(0x0099ff)
.setTitle(course + " " + courseTitle)
.setURL(url)
.setDescription(description)
.setImage("attachment://rating.png")
.addFields(ratings)
.setFooter({ text: numReviews });

await interaction.editReply({ embeds: [replyEmbed], files: [attachment] });
} catch (err) {
console.log(err);
await interaction.editReply({
content: `Sorry the course could not be found! 😔`,
});
}
},
};
2 changes: 1 addition & 1 deletion config/wordle.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
"players": []
}
}
Loading

0 comments on commit 0762214

Please sign in to comment.