How I Created a Discord Bot for ICAI Batch Notifications

Prashant JhaPrashant Jha
4 min read

My brother is studying for CA, and it is compulsory to take two study batches (Orientation and Information Technology) by ICAI. However, most of the time, the batches are full, and it can be tedious to check for availability continuously. So, I made a Discord bot for him, which can ping everyone on the server whenever seats are available in a batch.

First, I created a basic Discord bot using discord.js and CommandKit, a command handler for discord.js. Once the basic file and folder structure were ready, I created two helper files for easy maintenance of the code. I used Puppeteer to scrape the data from the ICAI website, Cheerio for parsing the HTML, and Bright Data. I only run the scraper every 30 minutes, so I may not need the use of Bright Data, but it makes the deployment easy because I am not using a private server. The server I use to deploy the bot (which I will tell you about later) does not allow me to install any software, and to use Puppeteer, I need to install Chromium. So, here Bright Data works well.

In the first helper file (scrapper.lib.js), a function called sleep waits in between the execution for the scrapper to work fine.

export const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));

Now for the main function, I called it getTableData because it scrapes the data from the website table. It accepts 3 parameters: region_id, pou_id, and courses_id. First, I set up the authentication for Bright Data, and then, in a try-catch block, connect to the browser, go to the URL, wait for the page to load, use the parameters to select the options for the data, get the HTML, load it into Cheerio, loop through it to get the data and return the data.

export const getTableData = async ({
    region_id,
    pou_id,
    courses_id
}) => {

    const AUTH = `${process.env.BRIGHT_DATA_USERNAME}:${process.env.BRIGHT_DATA_PASSWORD}`;
    const SBR_WS_ENDPOINT = `wss://${AUTH}@brd.superproxy.io:9222`;

    try {
        console.log('Connecting to Scraping Browser...');
        const browser = await puppeteer.connect({
            browserWSEndpoint: SBR_WS_ENDPOINT,
        });

        console.log('Connected! Navigating...');
        const page = await browser.newPage();

        const url = 'https://www.icaionlineregistration.org/LaunchBatchDetail.aspx'

        // Navigate the page to a URL
        await page.goto(url);

        // Set screen size
        await page.setViewport({ width: 1080, height: 1024 });

        await page.waitForSelector('#ddl_reg');

        await page.select('#ddl_reg', region_id)

        await page.waitForSelector('#ddlPou');

        await page.select('#ddlPou', pou_id)

        await page.select('#ddl_course', courses_id)

        await page.click('#btn_getlist')

        // get html from the page
        await page.waitForSelector('#GridView1');
        const html = await page.evaluate(() => document.body.innerHTML);

        const $ = cheerio.load(html);

        const tableData = []

        const tableHead = $('.gridHeader th');

        const tableContent = $('#GridView1 tbody tr');

        tableContent.map((i, el) => {
            if (i === 0) return;
            const obj = {}
            const td = $(el).find('td');
            td.map((index, elm) => {
                obj[$(tableHead[index]).text()] = $(elm).text().trim()
            })
            tableData.push(obj)
        });

        console.log('scraping done!')

        await browser.close();
        console.log('browser closed!')

        return tableData;
    } catch (error) {
        console.error(error);
        return null;
    }
}

In the first helper file (isSeatAvailable.lib.js), I created a function, called isSeatAvailable, I used this function to validate if there are any seats available for the Courses or not.

import { getTableData } from "./scraper.lib.js"

export const isSeatAvailable = async ({
    region_id,
    pou_id,
    courses_id,
}) => {

    const courses = await getTableData({
        region_id,
        pou_id,
        courses_id
    })

    let seatAvailable = false;
    let university = [];


    courses.forEach((course, index) => {
        if (course['Available Seats'] !== '0') {
            seatAvailable = true;
            university.push(courses[index])
        }
    })

    return {
        seatAvailable,
        university
    }
}

So now, the main functions are ready. I just need to run this function when I want to check for the seats. I need to check for it every 30 minutes; for that, I used the node-cron library. This code will only run if the bot is listening to the ready event, or we can say when the bot is online. I get the seats from the isSeatAvailable function and pass the parameters to it, which I get from the website itself. I fetch the channel with the channel ID in which I want to send the message, send 'No courses available' when no course is available at the time, otherwise send a message with the JSON. I just send JSON here to save time by not decorating it into an embed message.

import { isSeatAvailable } from "../../lib/isSeatAvailable.lib.js";
import cron from "node-cron";

export default function (c, client, handler) {
  cron.schedule("*/30 * * * *", async () => {
    const OrientationCourse = await isSeatAvailable({
      region_id: '3',
      pou_id: '254',
      courses_id: '46'
    })

    const InformationTechnology = await isSeatAvailable({
      region_id: "3",
      pou_id: "254",
      courses_id: "47",
    });

    console.log(OrientationCourse)
    console.log(InformationTechnology);

    c.channels
      .fetch("1241313007909339209")
      .then((channel) => {
        let courses = [];

        if (OrientationCourse.seatAvailable) {
          courses.push(OrientationCourse.university)
        }
        if (InformationTechnology.seatAvailable) {
          courses.push(InformationTechnology.university);
        }

        if (courses.length === 0) {
          channel.send({
            content: `No courses available\n**${JSON.stringify(courses)}**`,
          });
          console.log("No courses available");
          return;
        }

        channel
          .send({
            content: `**${JSON.stringify(
              courses,
              null,
              2
            )}** are available.\nThis Bot is created by <@962974774554918982>, Say thanks to him for this message. <@1253566986600976414> @everyone`,
          })
          .then((sentMessage) =>
            console.log(`Sent message: ${sentMessage.content}`)
          )
          .catch((error) => console.error(error));
      })
      .catch((error) => console.error(error));
  });
}

This is how the bot sends the messages.

My brother literally gets the batches from this and doesn't have to tediously check for availability continuously. From his first batch (Orientation), a new friend of his created a new Discord account just for it, and he also got the batch for Information Technology.

Thanks for reading ๐Ÿ˜Ž

1
Subscribe to my newsletter

Read articles from Prashant Jha directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Prashant Jha
Prashant Jha