diff --git a/.gitignore b/.gitignore index fdbe934..ed0046d 100644 --- a/.gitignore +++ b/.gitignore @@ -134,3 +134,4 @@ logs*.* course_resources/ RemoteAuth.zip dist/ +past_questions/ \ No newline at end of file diff --git a/src/commands/academic/pasco.ts b/src/commands/academic/pasco.ts new file mode 100644 index 0000000..f2d7d3b --- /dev/null +++ b/src/commands/academic/pasco.ts @@ -0,0 +1,230 @@ +import { IArgs, IClient } from "../../types"; +import { List, Message } from "whatsapp-web.js"; +import { getMutedStatus } from "../../models/misc"; +import { + PAST_QUESTIONS_REPLIES, + WAIT_REPLIES, + REACT_EMOJIS, +} from "../../utils/data"; +import { + currentEnv, + pickRandomReply, + currentPrefix, + sendPastQuestions, +} from "../../utils/helpers"; + +// Past Questions are referred to as "pasco" + +const execute = async (client: IClient, msg: Message, args: IArgs) => { + if ((await getMutedStatus()) === true) return; + + const { isListResponse, lastPrefixUsed } = args; + // if (currentEnv === 'production') { + const contact = await msg.getContact(); + const curChat = await msg.getChat(); + const chatFromContact = await contact.getChat(); + + if (curChat.isGroup) await msg.react(pickRandomReply(REACT_EMOJIS)); + + const list = new List( + "\nThis is a list of courses with available past questions", + "See courses", + [ + { + title: "", + rows: [ + // { + // id: + // lastPrefixUsed === process.env.DEV_PREFIX + // ? "pasco-416_dev" + // : "pasco-416_prod", + // title: "System Programming", + // description: "CSCD 416", + // }, + // { + // id: + // lastPrefixUsed === process.env.DEV_PREFIX + // ? "pasco-418_dev" + // : "pasco-418_prod", + // title: "Computer Systems Security", + // description: "CSCD 418", + // }, + { + id: + lastPrefixUsed === process.env.DEV_PREFIX + ? "pasco-422_dev" + : "pasco-422_prod", + title: "Human Computer Interaction", + description: "CSCD 422", + }, + // { + // id: + // lastPrefixUsed === process.env.DEV_PREFIX + // ? "pasco-424_dev" + // : "pasco-424_prod", + // title: "Management Principles", + // description: "CSCD 424", + // }, + // { + // id: + // lastPrefixUsed === process.env.DEV_PREFIX + // ? "pasco-400_dev" + // : "pasco-400_prod", + // title: "Project", + // description: "CSCD 400", + // }, + // { id: lastPrefixUsed === process.env.DEV_PREFIX ? 'pasco-426_dev' : 'pasco-426_prod', title: 'Multimedia Applications', description: 'CSCD 426' }, + // { + // id: + // lastPrefixUsed === process.env.DEV_PREFIX + // ? "pasco-428_dev" + // : "pasco-428_prod", + // title: "Expert Systems", + // description: "CSCD 428", + // }, + // { id: lastPrefixUsed === process.env.DEV_PREFIX ? 'pasco-432_dev' : 'pasco-432_prod', title: 'Concurrent & Distributed Systems', description: 'CSCD 432' }, + // { id: lastPrefixUsed === process.env.DEV_PREFIX ? 'pasco-434_dev' : 'pasco-434_prod', title: 'Mobile Computing', description: 'CSCD 434' }, + ], + }, + ], + pickRandomReply(PAST_QUESTIONS_REPLIES), + "Powered by Ethereal bot" + ); + + !isListResponse && (await chatFromContact.sendMessage(list)); + // } else { + // await msg.reply("The bot is currently hosted locally, so this operation cannot be performed.\n\nThe Grandmaster's data is at stake🐦") + // } + + if (isListResponse) { + if (msg.selectedRowId) { + const selectedRowId = msg.selectedRowId.split("-")[1]; + + console.log(`[SLIDES CMD] Slides from ${currentEnv} env`); + + switch (selectedRowId) { + case "416_dev": + if (currentEnv !== "development") break; + await msg.reply(pickRandomReply(WAIT_REPLIES)); // have to repeat this to avoid it leaking when bot environments are running simultaneously + sendPastQuestions(msg, "CSCD 416"); + break; + + case "416_prod": + if (currentEnv !== "production") break; + await msg.reply(pickRandomReply(WAIT_REPLIES)); + sendPastQuestions(msg, "CSCD 416"); + break; + + case "418_dev": + if (currentEnv !== "development") break; + await msg.reply(pickRandomReply(WAIT_REPLIES)); + sendPastQuestions(msg, "CSCD 418"); + break; + + case "418_prod": + if (currentEnv !== "production") break; + await msg.reply(pickRandomReply(WAIT_REPLIES)); + sendPastQuestions(msg, "CSCD 418"); + break; + + case "422_dev": + if (currentEnv !== "development") break; + await msg.reply(pickRandomReply(WAIT_REPLIES)); + sendPastQuestions(msg, "CSCD 422"); + break; + + case "422_prod": + if (currentEnv !== "production") break; + await msg.reply(pickRandomReply(WAIT_REPLIES)); + sendPastQuestions(msg, "CSCD 422"); + break; + + case "424_dev": + if (currentEnv !== "development") break; + await msg.reply(pickRandomReply(WAIT_REPLIES)); + sendPastQuestions(msg, "CSCD 424"); + break; + + case "424_prod": + if (currentEnv !== "production") break; + await msg.reply(pickRandomReply(WAIT_REPLIES)); + sendPastQuestions(msg, "CSCD 424"); + break; + + case "400_dev": + if (currentEnv !== "development") break; + await msg.reply(pickRandomReply(WAIT_REPLIES)); + sendPastQuestions(msg, "CSCD 400"); + break; + + case "400_prod": + if (currentEnv !== "production") break; + await msg.reply(pickRandomReply(WAIT_REPLIES)); + sendPastQuestions(msg, "CSCD 400"); + break; + + case "426_dev": + if (currentEnv !== "development") break; + await msg.reply(pickRandomReply(WAIT_REPLIES)); + sendPastQuestions(msg, "CSCD 426"); + break; + + case "426_prod": + if (currentEnv !== "production") break; + await msg.reply(pickRandomReply(WAIT_REPLIES)); + sendPastQuestions(msg, "CSCD 426"); + break; + + case "428_dev": + if (currentEnv !== "development") break; + await msg.reply(pickRandomReply(WAIT_REPLIES)); + sendPastQuestions(msg, "CSCD 428"); + break; + + case "428_prod": + if (currentEnv !== "production") break; + await msg.reply(pickRandomReply(WAIT_REPLIES)); + sendPastQuestions(msg, "CSCD 428"); + break; + + case "432_dev": + if (currentEnv !== "development") break; + await msg.reply(pickRandomReply(WAIT_REPLIES)); + sendPastQuestions(msg, "CSCD 432"); + break; + + case "432_prod": + if (currentEnv !== "production") break; + await msg.reply(pickRandomReply(WAIT_REPLIES)); + sendPastQuestions(msg, "CSCD 432"); + break; + + case "434_dev": + if (currentEnv !== "development") break; + await msg.reply(pickRandomReply(WAIT_REPLIES)); + sendPastQuestions(msg, "CSCD 434"); + break; + + case "434_prod": + if (currentEnv !== "production") break; + await msg.reply(pickRandomReply(WAIT_REPLIES)); + sendPastQuestions(msg, "CSCD 434"); + break; + + default: + break; + } + } + + args.isListResponse = false; + } +}; + +module.exports = { + name: "pasco", + description: "Get course materials for all courses πŸ“š", + alias: ["pascos", "pq"], + category: "everyone", // admin | everyone + help: `To use this command, type:\n*${currentPrefix}pasco*, then choose a course from the list provided`, + execute, +}; diff --git a/src/main.ts b/src/main.ts index 10c2ac5..8b45bc0 100644 --- a/src/main.ts +++ b/src/main.ts @@ -219,6 +219,10 @@ if (process.env.MONGO_URL) { client.commands && client.commands.get("slides")?.execute(client, msg, args); break; + case "pasco": + client.commands && + client.commands.get("pasco")?.execute(client, msg, args); + break; case "class": client.commands && client.commands.get("class")?.execute(client, msg, args); diff --git a/src/models/pastQuestions.ts b/src/models/pastQuestions.ts new file mode 100644 index 0000000..e453f98 --- /dev/null +++ b/src/models/pastQuestions.ts @@ -0,0 +1,59 @@ +// -------------------------------------------------- +// pastQuestions.ts contains the schema for course past questions +// -------------------------------------------------- + +const { Schema, model } = require('mongoose'); +const fs = require('fs'); + + +/** + * Schema for course past questions + */ +const PastQuestionsSchema = new Schema({ + title: String, + courseCode: String, + binData: String, // when Buffer is used, puppeteer complains that it's not properly encoded +}); + +const prodModelName = "pq-files"; +const PastQuestionsModel = model(prodModelName, PastQuestionsSchema); +//todo: Since we can't push `past_questions`, we should only allow uploading courses to development collection + +/** + * Helper function to encode the local courses materials and save them on the cloud database. + * @async + */ +const initCollection = async () => { + const count = await PastQuestionsModel.countDocuments({}); + console.log('[PAST_QUESTIONS MODEL] Doc count:', count); + if (!count) { + const dir = './past_questions'; + for (const folder of fs.readdirSync(dir)) { + console.log('[PAST_QUESTIONS MODEL]', folder) + const courseCode = folder.split('-')[0].trim(); + for (const file of fs.readdirSync(dir + '/' + folder)) { + const binData = fs.readFileSync(dir + '/' + folder + '/' + file, { encoding: 'base64' }); + const doc = new PastQuestionsModel({ title: file, courseCode, binData }); + try { + await doc.save(); + } catch (err) { + console.error('[PAST_QUESTIONS MODEL ERROR]', err); + } + } + } + console.log("[PAST_QUESTIONS MODEL] Done encoding all past questions!"); + } else console.log('[PAST_QUESTIONS MODEL]', prodModelName + " collection is not empty"); +} +initCollection(); + + +/** + * Gets specific past questions from the database. + * @param {string} courseCode String representing the course code for a specific course + * @returns + */ +export const getPastQuestions = async (courseCode: string) => { + const res = await PastQuestionsModel.find({ courseCode }); + // console.log('[PAST_QUESTIONS MODEL]', res); + return res; +} \ No newline at end of file diff --git a/src/utils/data.ts b/src/utils/data.ts index 3561bf8..0831c69 100644 --- a/src/utils/data.ts +++ b/src/utils/data.ts @@ -386,6 +386,19 @@ export const COURSE_MATERIALS_REPLIES = [ "Your wish is my command 🐦", ]; +/** + * Array containing replies to `!slides` command. + */ +export const PAST_QUESTIONS_REPLIES = [ + "Need some past questions?", + "Oh you need pasco? 🐦", + "Need any past questions?", + "What past questions can I help you with?", + "Looking for pasco?", + "Your search for pasco ends here 🐦", + "Your wish is my command 🐦", +]; + /** * Map containing some extra info about the bot/random messages to be sent to users * based on weighted chances. diff --git a/src/utils/helpers.ts b/src/utils/helpers.ts index a1bb91d..cff42c9 100644 --- a/src/utils/helpers.ts +++ b/src/utils/helpers.ts @@ -13,6 +13,7 @@ import { MIME_TYPES, ALL_CLASSES } from "./data"; import { getResource } from "../models/resources"; import { IClient, ICourse } from "../types"; import { TCommands } from "../types"; +import { getPastQuestions } from "../models/pastQuestions"; // GLOBAL VARIABLES ---------------------------------- /** @@ -647,6 +648,43 @@ const sendSlides = async (msg: Message, courseCode: string) => { } }; +/** + * Helper function to retrieve past questions from DB to send to user. + * @param msg Message object from whatsapp. + * @param courseCode String representing course code. + * @async + */ +const sendPastQuestions = async (msg: Message, courseCode: string) => { + let isDone = false; + console.log("[HELPERS - SS] Getting past questions..."); + const materials = await getPastQuestions(courseCode); + if (materials.length) console.log(" [HELPERS - SS]Got past questions"); + else console.error(" [HELPERS - SS ERROR] No past questions received from DB"); + for (const material of materials) { + const curMaterial = material; + const file_extension = + curMaterial.title.split(".")[curMaterial.title.split(".").length - 1]; // always extract the last "." and what comes after + const foundMimeType = MIME_TYPES.find( + (obj) => obj.fileExtension === file_extension + ); + + if (foundMimeType) { + const { mime_type } = foundMimeType; + const slide = new MessageMedia( + mime_type, + curMaterial.binData, + curMaterial.title + ); + await msg.reply(slide); + console.log("[HELPERS - SS] Sent a past question"); + if (material === materials[materials.length - 1]) isDone = true; + } + // if (isDone) await msg.reply(`Done πŸ‘πŸ½ from ${currentEnv}`); + if (isDone) await msg.reply(`Done πŸ‘πŸ½`); + console.log(" [HELPERS - SS]Done sending past questions"); + } +}; + /** * Checks whether a user is a bot admin. User passed can be a whatsapp contact or a whatsapp number as a String. * @param contact Object that represents a contact on whatsapp. @@ -881,4 +919,5 @@ export { getTimeLeftForSetTimeout, checkForSpam, checkForChance, + sendPastQuestions, };