KIIT-Connect Unlocker

Unlock kiitconnect for free

// ==UserScript==
// @name         KIIT-Connect Unlocker
// @namespace    https://github.com/rohitaryal/kiitconnect-unlocker
// @version      3.0
// @description  Unlock kiitconnect for free
// @author       rohitaryal
// @match        https://www.kiitconnect.com/academic/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=kiitconnect.com
// @grant        none
// @license      MIT
// ==/UserScript==

const debugMode = true;

const driveFile = "https://drive.google.com/file/d/";
const driveFolder = "https://drive.google.com/drive/folders/";
const youtubePlaylist = "https://www.kiitconnect.com/player?playlistId=";

const pyqsRegex = /academic\/PYQS\/Content\/PYQS\-(cse|csse|csce|it)\-[1-7]/;
const notesRegex = /academic\/PYQS\/Content\/Notes\-(cse|csse|csce|it)\-[1-7]/;

class Utils {
  constructor() { }
  /*
   * Get all script that doesn't have src attribute
   */
  static getScripts() {
    let scriptElements = document.querySelectorAll("script:not([src])");
    return [...scriptElements];
  }

  /*
   * Copy item to clipboard
   */
  static copy(text) {
    document.body.focus();
    navigator.clipboard.writeText(text);
  }

  /*
   * Logger for debug mode
   */
  static log(text) {
    if (debugMode) {
      console.log(text)
    }
  }

  /*
   * Clone node and remove its junk and replace with new one
   */
  static replaceNode(node) {
    if (!node) {
      throw new Error("Invalid node provided to clone");
    }

    const newNode = node.cloneNode(true);
    node.replaceWith(newNode);

    return newNode;
  }

  /*
   * Re-Serialize the json in our own way (For PYQS)
   */
  static parsePYQ(data) {
    if (typeof data !== 'object') {
      throw new Error(`Expected 'object' but obtained '${typeof data}' while parsing`);
    }

    // Skeleton for subjects pyq, syllabus + some extra detail holder
    let pyqDetails = {
      semester: data.semeseter, // Yes, thats the actual spelling
      branch: data.stream,
      subjects: [],
    };

    pyqDetails.subjects = data.data.semesters[0].subjects.map((subject) => {
      // Skeleton for subject detail holder
      let subDetails = {
        subjectName: subject.name,
        subjectCode: subject.SUBCODE,
        folder: driveFolder + subject.folderId, // Makes an accessable drive folder link
        subjectSyllabus: driveFile + subject.syllabus, // Makes an accessable drive file link
        playlist: [],
        papers: [],
      };

      // Parse individual playlist
      subDetails.playlist = subject.youtubePlaylist.map((playlist) => {
        return {
          id: playlist.id,
          title: playlist.title,
          videoCount: playlist.noOfVides,
          videoLink: `${youtubePlaylist}id&playlist=${playlist.title}`
        };
      });

      // Parse individual pyqs
      subDetails.papers = subject.pyqs.map((paper) => {
        return {
          name: paper.name,
          year: Number(paper.year) || 0,
          solution: driveFile + paper.solution, // Makes an accessable file link
          question: driveFile + paper.Question, // Same
        }
      });

      // Sort papers based on years
      subDetails.papers = subDetails.papers.sort((a, b) => a.year - b.year);

      return subDetails;
    });

    return pyqDetails;
  }

  /*
   * Re-Serialize the json in our own way (For Notes)
   */
  static parseNotes(data) {

  }

  /*
   * Parse script content (For PYQS)
   */
  static parseFromScriptPYQ(scriptContent) {
    // Pre-process and beautufy
    scriptContent = scriptContent.replaceAll(`\\"`, `"`);
    scriptContent = scriptContent.replaceAll(`\\n`, ``);

    const parsableContent = scriptContent.slice(
      scriptContent.indexOf(`self.__next_f.push([1,"1e:`) + 26, // <-- 26 is the length of this
      scriptContent.lastIndexOf(`"])`)                          //     string itself.
    );

    const jsonContent = JSON.parse(parsableContent)[3].children[3];

    return this.parsePYQ(jsonContent);
  }

  /*
   * Parse script content (For Notes)
   */
  static parseFromScriptNotes(scriptContent) {

  }

  /*
   * Replace pyq table + headings with from our own serialized form
   */
  static rewritePYQPage(subjectArray) {
    if (typeof subjectArray !== 'object') {
      throw new Error(`Expected 'object' but obtained '${typeof subjectArray}' while rewriting`);
    }

    // After successful injection head will contain this classlist
    // which is an indicator to stop further injection
    if(document.head.classList.contains("injected")) {
      return;
    }

    Utils.log("Re-writing the pyq page.")

    const parentContainer = [...document.querySelector("main div div").childNodes].slice(1);
    
    for (let i = 0; i < parentContainer.length; i++) {
      let totalRows = parentContainer[i].querySelectorAll("table tbody").length; // Get count of rows
      for (let j = 0; j < subjectArray.length; j++) {
          let totalPapers = subjectArray[j].papers.length;
  
          if (totalRows === totalPapers) {
              let temp = subjectArray[j];
              subjectArray[j] = subjectArray[i];
              subjectArray[i] = temp;
              break;
          }
      }
  }
  

    for (let i = 0; i < subjectArray.length; i++) {
      const subject = subjectArray[i];
      const parent = parentContainer[i];
      const [titleDiv, tableContainer] = parent.childNodes;

      const syllabusButton = titleDiv.querySelector("a");
      syllabusButton.href = subject.subjectSyllabus;

      // New button with folder link
      const folderButton = syllabusButton.cloneNode(true);
      folderButton.innerText = "Open Folder";
      folderButton.href = subject.folder;

      
      // Modify title (middle one is TEXT_NODE)
      const title = titleDiv.childNodes[1];
      title.textContent = subject.subjectName;
      
      // Modify the table
      const tableRows = tableContainer.querySelectorAll("table tbody");

      for (let j = 0; j < tableRows.length; j++) {
        const row = tableRows[j];
        const paper = subject.papers[j];

        const td = row.querySelectorAll("td:not(.hidden)");

        // Year of paper
        td[0].textContent = paper.year;

        // Link to question paper
        let paperAnchor = td[1].querySelector("a");
        paperAnchor = this.replaceNode(paperAnchor);

        paperAnchor.textContent = paper.name;
        paperAnchor.href = paper.question;
        paperAnchor.classList.add("text-cyan-500");
        paperAnchor.classList.remove("text-yellow-500", "cursor-not-allowed");

        // Link to solution paper
        let solutionAnchor = td[2].querySelector("a");

        // Since solution is the last element
        if(!solutionAnchor)
          continue;

        solutionAnchor = this.replaceNode(solutionAnchor);

        if (solutionAnchor.classList.contains("text-gray-400")) {
          solutionAnchor.textContent = "Not Available";
        } else {
          solutionAnchor.classList.add("text-cyan-500");
          solutionAnchor.classList.remove("text-yellow-500", "cursor-not-allowed");

          solutionAnchor.href = paper.solution
        }
      }
    }

    // Mark as succrssful injection
    document.head.classList.add("injected");
  }
}

(function () {
  'use strict';
  // Last script will always contain the paper's JSON
  let scriptContent = Utils.getScripts().at(-1).textContent;


  if (!!location.pathname.match(pyqsRegex)) { // Means we are in 'pyqs' page
    const parsedContent = Utils.parseFromScriptPYQ(scriptContent);

    window.addEventListener('load', function () {
      Utils.log("DOMContent has been loaded");
      this.setInterval(function () {
        try {
          Utils.rewritePYQPage(parsedContent.subjects);
        } catch(e) {
          console.log(e)
        }
      }, 2000)
    });

  } else if (!!location.pathname.match(notesRegex)) { // Means we are in 'notes' page

  }
})();