Schoology Grades

Grabs Grades from Schoology

// ==UserScript==
// @name         Schoology Grades
// @version      2.0
// @description  Grabs Grades from Schoology
// @author       Hafnium780
// @match        https://*.schoology.com/sg
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_xmlhttpRequest
// @license      MIT
// @namespace    hafnium780/sg
// ==/UserScript==

(function () {
  document.title = "sg";
  window.stop();
  document.body = document.createElement("body");
  // Make new page
  document.body.innerHTML = `
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap" rel="stylesheet">
<style>
* { margin: 0px; padding: 0px; font-family: Inter; align-items: center; text-align: center; }
.nosel { -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: -moz-none; -o-user-select: none; user-select: none; }
input { outline: none !important }

::-webkit-scrollbar { width: 10px; }
::-webkit-scrollbar-track { background: #1E1F21; }
::-webkit-scrollbar-thumb { background: #363639; border-radius: 5px; }

html { overflow: hidden; }
body { padding: 0px; }

#bar { background-color: #363639; padding: 6px; height: 65px; top: 0px; left: 0px; right: 0px; border-bottom: 2px solid #424244; }

#title { color: #F5F4F3; font-size: 25px; margin: 6px 0px; font-weight: 700; }

#domain { margin-top: 3px; }
#domain-input { width: 50px; height: 12px; padding-bottom: -2px; font-size: 13px; border: none; border-radius: 2px; background-color: #161619; color: #F5F4F3; }
#domain-text { font-size: medium; color: #F5F4F3; }

#secrets { display: flex; position: absolute; height: 50px; width: 450px; transition: left ease 200ms; bottom: 0px; left: 0px; border: none; }
#secrets-input { background-color: #363639; width: 435px; padding: 4px; height: 42px; border: none; border-top: 2px solid #424244; }
#key { font-size: medium; padding-left: 47px; text-align: left; color: #F5F4F3; }
#key-input { width: 310px; height: 14px; background-color: #161619; border: 1px solid black; font-family: monospace; font-size: small; border: none; color: #F5F4F3; }
#signature { font-size: medium; padding-left: 3px; text-align: left; color: #F5F4F3; }
#signature-input { width: 310px; height: 14px; background-color: #161619; border: 1px solid black; font-family: monospace; font-size: small; border: none; color: #F5F4F3; }

#secrets-toggle { writing-mode: vertical-rl; text-orientation: mixed; color: #F5F4F3; width: 15px; background-color: #F9485F; height: 46px; padding: 2px; cursor: pointer; font-size: 12px; line-height: 1.25; border-top-right-radius: 10px; border-bottom-right-radius: 10px; }

#grades { display: flex; align-items: start; }

#course-grades { overflow: auto; width: 450px; height: calc(100vh - 65px); background-color: #2E2E30; border-right: 2px solid #424244; }

.course-grade-element:first-child { margin-top: 10px; }
.course-grade-element { font-size: 14px; display: flex; height: 40px; cursor: pointer; filter: brightness(100%); margin: 8px; background-color: #2E2E30; border-radius: 10px; }
.course-grade-name { width: 80%; text-align: left; padding-left: 10px; color: #F5F4F3; }
.course-grade-grade { width: 14%; text-align: center; padding: 5px; background-color: #E9384F; color: #F5F4F3; border-radius: 10px; margin-left: 16px; }


.course-grade-element:hover { filter: brightness(100%); background-color: #5A5A5C; transition: filter ease 300ms, background-color ease 300ms; }

#assignment-div { width: calc(100vw - 450px); height: calc(100vh - 65px); background-color: #1E1F21; }
#assignment-grades { overflow: auto; width: 100%; height: calc(100vh - 180px); background-color: #1E1F21; }
#calculated-grade-title { font-size: large; color: #F5F4F3; margin: 6px 0px; }
#calculated-grade { font-size: x-large; color: #F5F4F3; padding: 5px; background-color: #E9384F; border-radius: 10px; width: min-content; margin: 6px auto; }
#calculated-grade-error { font-size: small; color: #F5F4F3; margin: 6px 0px; }

.categorytitlediv { border-top: 2px solid #424244; position: sticky; background-color: #2E2E30;
                    top: 0px; display: flex; flex-direction: row; margin-bottom: 3px; padding: 5px; color: #F5F4F3; z-index: 100; }
.categorypoints { font-size: small; flex: 0 0; text-align: center; padding: 5px; background-color: #E9384F; border-radius: 10px; min-width: 100px; margin-left: 10px; }
.categorypercent { font-size: small; flex: 0 0; text-align: center; padding: 5px; background-color: #C1B4AE; color: #1E1F21; border-radius: 10px; min-width: 60px; }
.categorytitle { font-size: large; font-weight: bold; flex: 1 1; text-align: center; position: absolute; left: 0px; right: 0px; pointer-events: none; }
.categoryweight { font-size: small; padding: 5px; text-align: center; background-color: #C1B4AE; color: #1E1F21; border-radius: 10px; flex: 0 0; min-width: 100px; margin-left: 10px; margin-right: calc(100% - 423px); }
.categoryweightmethod { cursor: pointer; font-size: small; padding: 5px; text-align: center; background-color: #E9384F; border-radius: 10px; flex: 0 0; min-width: 100px; transition: background-color ease 200ms; }
.categoryweightmethod:hover { background-color: #F9586F; }
.categoryweight:empty { opacity: 0; }
.categorydiv:last-child { padding-bottom: 15px; }

.assignment-grade-element { padding: 6px; padding-left: 10px; display: flex; min-width: 0; flex-shrink: 0; flex-grow: 0; align-items: center; margin-right: 2px; border-top: 2px solid #424244; }
.assignment-grade-element:first-child { border: none; }
.assignment-grade-input { width: 55px; background-color: #931022; font-size: 13px; color: #F5F4F3; border: none; padding: 4px 3px 4px 4px; height: 18px; flex: 0 0 55px; text-align: right; border-radius: 5px; }
.assignment-grade-input.assignment-grade-input:focus { background-color: #531012 }
.assignment-grade-input:hover { background-color: #731012 }
::placeholder { color: #F5F4F3; font-size: 13px; }
.assignment-grade-max { flex: 0 0 52px; text-align: left; color: #F5F4F3; font-size: 13px; }
.assignment-grade-grade { display: flex; align-items: center; justify-content: end; font-size: small; text-align: right; flex-grow: 0; flex-shrink: 0; vertical-align: middle; text-align: center; padding-right: 10px; padding: 5px; background-color: #931022; border-radius: 10px; width: 100px; height: 16px; overflow: hidden; }
a.assignment-grade-name { color: #F5F4F3 !important; }
.assignment-grade-name { color: #F5F4F3; text-align: left; flex-grow: 1; margin-right: 3px; margin-left: 3px; font-size: medium; font-weight: normal; }
.assignment-grade-reset { width: 16px; height: 16px; background-color: #E9384F; flex: 0 0 16px; margin-right: 10px; cursor: pointer; scale: 1; border-radius: 7px; color: #F5F4F3 }
.assignment-grade-reset:hover { scale: 1.1; transition: scale ease 100ms; }
.assignment-grade-reload { border-radius: 8px; width: 16px; height: 16px; font-size: 12px; background-color: #767679; flex: 0 0 16px; left: 2px; text-align: center; color: #F5F4F3; margin: 0px 2px; cursor: pointer; scale: 1; padding: auto; }
.assignment-grade-reload:hover { scale: 1.1; transition: scale ease 100ms; }

</style>
<div id="bar" class="nosel">
  <div id="title"></div>
  <div id="domain">
    <span id="domain-text">Domain: </span>
    <input type="text" id="domain-input"></input>
    <span id="domain-text">.schoology.com</span>
  </div>
</div>
<div id="secrets" class="nosel">
  <div id="secrets-input">
    <div id="key">
      Key:
      <input id="key-input"></input>
    </div>
    <div id="signature">
      Signature:
      <input id="signature-input"></input>
    </div>
  </div>
  <div id="secrets-toggle">API</div>
</div>
<div id="grades">
  <div id="course-grades" class="nosel">
  </div>
  <div id="assignment-div">
    <div id="assignment-grades">

    </div>
  </div>
</div>
    `;
  const protocol = "https";

  let grades,
    courses,
    assignments,
    assignmentNames,
    assignmentFactors,
    categoryTypes;

  let assignmentShowID = undefined;

  const title = document.getElementById("title");
  const setTitle = (t) => {
    title.innerText = t;
  };

  // Get DOM elements

  const domain_input = document.getElementById("domain-input");
  const secrets = document.getElementById("secrets");
  const key_input = document.getElementById("key-input");
  const signature_input = document.getElementById("signature-input");
  const secrets_toggle = document.getElementById("secrets-toggle");
  const course_grades = document.getElementById("course-grades");
  const assignment_div = document.getElementById("assignment-div");
  const assignment_grades = document.getElementById("assignment-grades");

  // Before loading grades

  const authError = () => {
    setTitle("↙ Enter API Key and Signature");
  };

  const start = () => {
    if (!key || !signature) {
      authError();
      return;
    }
    setTitle("Loading Grades...");
    loadGrades();
  };

  // Load and save input values

  const storeAssignmentDetails = () => {
    GM_setValue("sg_assignment_names", assignmentNames);
    GM_setValue("sg_assignment_factors", assignmentFactors);
  };

  const storeCategoryDetails = () => {
    GM_setValue("sg_category_types", categoryTypes);
  };

  let key = GM_getValue("sg_key");
  let signature = GM_getValue("sg_signature");
  let domain = GM_getValue("sg_domain");

  if (domain) domain_input.value = domain;
  else {
    domain = new URL(window.location.href).host.split(".")[0];
    GM_setValue("sg_domain", domain);
    domain_input.value = domain;
  }

  domain_input.addEventListener("input", () => {
    GM_setValue("sg_domain", domain_input.value);
    domain = domain_input.value;
  });

  if (key) key_input.value = key;
  if (signature) signature_input.value = signature;

  if (key && signature) secrets.style.left = "-435px";

  key_input.addEventListener("input", () => {
    GM_setValue("sg_key", key_input.value);
    key = key_input.value;
    start();
  });

  signature_input.addEventListener("input", () => {
    GM_setValue("sg_signature", signature_input.value);
    signature = signature_input.value;
    start();
  });

  secrets_toggle.addEventListener("click", () => {
    if (secrets.style.left === "-435px") secrets.style.left = "0px";
    else secrets.style.left = "-435px";
  });

  const authHeader = () => {
    return (
      `OAuth realm="Schoology API",oauth_consumer_key="` +
      key +
      `",oauth_token="",oauth_nonce="` +
      Math.floor(Math.random() * 100000000) +
      `",oauth_timestamp="` +
      Math.floor(Date.now() / 1000) +
      `",oauth_signature_method="PLAINTEXT",oauth_version="1.0",oauth_signature="` +
      signature +
      `%26"`
    );
  };

  const schoologyHeaders = (fileType) => {
    return {
      Accept: fileType,
      Host: "api.schoology.com",
      Authorization: authHeader(),
      "Content-Type": "text/xml",
    };
  };

  const setHeaders = (req, fileType) => {
    req.setRequestHeader("Accept", fileType);
    req.setRequestHeader("Host", "api.schoology.com");
    req.setRequestHeader("Authorization", authHeader());
  };

  // API helper functions

  const getContent = (a, xml = false) => {
    return new Promise((res, rej) => {
      let link =
        a.indexOf(protocol + "://api.schoology.com") === -1
          ? protocol + "://api.schoology.com/v1/" + a
          : a;

      GM_xmlhttpRequest({
        url: link,
        method: "GET",
        headers: schoologyHeaders(xml ? "text/xml" : "application/json"),
        onload: (r) => {
          res(xml ? r.responseText : JSON.parse(r.responseText));
        },
        onerror: (e) => {
          rej(e);
        },
      });
    });
  };

  const getHTML = (a) => {
    return new Promise((res, rej) => {
      GM_xmlhttpRequest({
        url: a,
        method: "GET",
        headers: schoologyHeaders("text/html"),
        onload: (r) => {
          res(r.responseText);
        },
        onerror: (e) => {
          rej(e);
        },
      });
    });
  };

  const multiGet = (b) => {
    return new Promise((res, rej) => {
      GM_xmlhttpRequest({
        url: protocol + "://api.schoology.com/v1/multiget",
        method: "POST",
        headers: schoologyHeaders("application/json"),
        data: b,
        onload: (r) => {
          res(JSON.parse(r.responseText));
        },
        onerror: (e) => {
          rej(e);
        },
      });
    });
  };

  const roundGrade = (g) => {
    if (g && !isNaN(g)) return g.toFixed(2) + "%";
    return "N/A%";
  };

  const formatGrade = (a) => {
    return (a.grade ?? "---") + "/" + (a.max_points ?? "---");
  };

  // Custom display elements

  const createCourseGrade = (name, grade, courseI) => {
    let mainDiv = document.createElement("div");
    mainDiv.classList.add("course-grade-element");
    let nameDiv = document.createElement("div");
    nameDiv.innerText = name;
    nameDiv.classList.add("course-grade-name");
    let gradeDiv = document.createElement("div");
    gradeDiv.innerText = grade;
    gradeDiv.classList.add("course-grade-grade");
    mainDiv.appendChild(gradeDiv);
    mainDiv.appendChild(nameDiv);
    course_grades.appendChild(mainDiv);

    mainDiv.addEventListener("click", () => {
      loadAssignments(courses[courseI]);
    });
  };

  const createAssignmentGrade = (name, grade, url = undefined) => {
    let mainDiv = document.createElement("div");
    mainDiv.classList.add("assignment-grade-element");
    let nameDiv = document.createElement(url ? "a" : "div");
    nameDiv.innerText = name;
    nameDiv.classList.add("assignment-grade-name");
    if (url) nameDiv.href = url;
    let gradeDiv = document.createElement("div");
    gradeDiv.innerText = grade;
    gradeDiv.classList.add("assignment-grade-grade");
    mainDiv.appendChild(gradeDiv);
    mainDiv.appendChild(nameDiv);
    assignment_grades.appendChild(mainDiv);
  };

  // Main loading and display

  const displayGrades = () => {
    course_grades.innerHTML = "";
    for (const grade of grades) {
      createCourseGrade(grade.courseName, grade.grade, grade.courseI);
    }
  };

  const displayAssignments = (s, id) => {
    if (assignmentShowID !== id) return;

    categoryTypes = {};
    try {
      categoryTypes = GM_getValue("sg_category_types") ?? {};
    } catch (e) {}
    assignment_grades.innerHTML = "";
    let calculated_grade_title =
      document.getElementById("calculated-grade-title") ??
      document.createElement("div");
    let calculated_grade =
      document.getElementById("calculated-grade") ??
      document.createElement("div");
    let calculated_grade_error =
      document.getElementById("calculated-grade-error") ??
      document.createElement("div");
    calculated_grade_title.id = "calculated-grade-title";
    calculated_grade.id = "calculated-grade";
    calculated_grade_error.id = "calculated-grade-error";
    calculated_grade_title.innerText = "Calculated Grade";
    calculated_grade_error.innerText =
      "Category grades not matching with Schoology? Try switching the weighting method (Percentage or Points) for individual categories.";
    assignment_div.insertBefore(calculated_grade_title, assignment_grades);
    assignment_div.insertBefore(calculated_grade, assignment_grades);
    assignment_div.insertBefore(calculated_grade_error, assignment_grades);
    const categories = [];
    if (!s.grading_category[s.grading_category.length - 1].created) {
      s.grading_category.push({
        id: undefined,
        title: "Other",
        created: true,
      });
    }
    let totalWeight = 0;
    for (const gc of s.grading_category) {
      if (gc.weight) totalWeight += gc.weight;
    }

    for (let i = 0; i < s.grading_category.length; i++) {
      const gc = s.grading_category[i];
      let weight = undefined;
      if (totalWeight === 100) {
        if (s.grading_category[i].weight)
          weight = s.grading_category[i].weight / totalWeight;
        else weight = 0;
      }
      const div = document.createElement("div");
      const title = document.createElement("div");
      const titlediv = document.createElement("div");
      const wdiv = document.createElement("div");
      const methoddiv = document.createElement("div");
      wdiv.classList.add("categoryweight");
      wdiv.innerText = weight
        ? "Weight: " + +parseFloat(weight * 100).toFixed(2) + "%"
        : "";
      methoddiv.classList.add("categoryweightmethod");
      methoddiv.classList.add("nosel");
      methoddiv.innerText =
        (categoryTypes[gc.id] ?? 2) == 1 ? "Percentage" : "Points";

      let id = gc.id;
      methoddiv.addEventListener("click", () => {
        categoryTypes[id] = (categoryTypes[gc.id] ?? 2) == 1 ? 2 : 1;
        methoddiv.innerText =
          categoryTypes[gc.id] == 1 ? "Percentage" : "Points";
        storeCategoryDetails();
        updateGrades(s, categories);
      });

      title.classList.add("categorytitle");
      titlediv.classList.add("categorytitlediv");
      div.classList.add("categorydiv");
      div.style.zIndex = i.toString();
      title.innerText = gc.title;
      assignment_grades.appendChild(div);
      div.appendChild(titlediv);
      titlediv.appendChild(methoddiv);
      titlediv.appendChild(wdiv);
      titlediv.appendChild(title);
      categories.push({
        id: gc.id,
        title: gc.title,
        div: div,
        weight: weight,
        totPts: 0,
        maxPts: 0,
      });
    }

    for (let i = 0; i < assignments.length; i++) {
      const a = assignments[i];
      const div = document.createElement("div");
      const grade = document.createElement("div");
      const gradeMax = document.createElement("div");
      const gradeInput = document.createElement("input");
      const resetGrade = document.createElement("div");
      const reloadName = document.createElement("div");
      let assignmentName;
      if (a.url) {
        assignmentName = document.createElement("a");
        assignmentName.href = a.url;
        assignmentName.target = "_blank";
      } else {
        assignmentName = document.createElement("div");
      }
      div.classList.add("assignment-grade-element");
      grade.classList.add("assignment-grade-grade");
      gradeMax.classList.add("assignment-grade-max");
      gradeInput.classList.add("assignment-grade-input");
      resetGrade.classList.add("assignment-grade-reset");
      assignmentName.classList.add("assignment-grade-name");
      reloadName.classList.add("assignment-grade-reload");

      reloadName.innerText = String.fromCodePoint(8635); // ↻
      gradeMax.innerText = "/ " + (a.rawGrade.max ?? "---");
      gradeInput.value = a.rawGrade.pts ?? "";
      gradeInput.setAttribute("placeholder", "---");
      resetGrade.innerText = String.fromCodePoint(8635); // ↻

      if (a.assignmentName == undefined || a.assignmentName == "le>")
        assignmentName.innerText = "unknown";
      else assignmentName.innerText = a.assignmentName;
      const cat = categories.find((e, i) => {
        return e.id === a.category_id || i == categories.length - 1;
      });

      cat.div.appendChild(div);
      reloadName.addEventListener("click", () => {
        assignmentName.innerText = "";
        getAssignmentTitles([a], s, id, true);
      });

      gradeInput.addEventListener("input", () => {
        updateGrades(s, categories);
      });

      resetGrade.addEventListener("click", () => {
        gradeInput.value = a.rawGrade.pts ?? "";
        updateGrades(s, categories);
      });

      div.appendChild(reloadName);
      div.appendChild(assignmentName);
      div.appendChild(resetGrade);
      div.appendChild(grade);
      grade.appendChild(gradeInput);
      grade.appendChild(gradeMax);
      a.gradeInput = gradeInput;
      a.resetGrade = resetGrade;
    }
    updateGrades(s, categories);
    //        for (const assignment of assignments) {
    //            createAssignmentGrade(assignment.assignmentName, assignment.grade, assignment.url);
    //        }
  };

  const updateGrades = (s, categories, reset = false) => {
    for (const c of categories) {
      c.totPts = 0;
      c.maxPts = 0;
      c.calculation_type = categoryTypes[c.id] ?? 2;
    }

    for (let i = 0; i < assignments.length; i++) {
      const a = assignments[i];
      const cat = categories.find((e, i) => {
        return e.id === a.category_id || i == categories.length - 1;
      });
      const pts = reset ? a.rawGrade.pts : parseFloat(a.gradeInput.value);
      if (reset) a.gradeInput.value = a.rawGrade.pts ?? "";
      if (!isNaN(pts) && pts !== null) {
        if (cat.calculation_type == 2) {
          cat.totPts += pts * a.factor;
          cat.maxPts += a.rawGrade.max * a.factor;
        } else {
          cat.totPts += (pts / a.rawGrade.max) * a.factor;
          cat.maxPts += a.factor;
        }
      }
      if (
        (isNaN(pts) && a.rawGrade.pts !== null) ||
        (!isNaN(pts) && pts !== a.rawGrade.pts)
      ) {
        a.resetGrade.style.display = "block";
      } else {
        a.resetGrade.style.display = "none";
      }
    }

    let calcGrade;
    if (categories[0].weight !== undefined) {
      let totPer = 0;
      let outOf = 1;
      for (const gc of categories) {
        if (gc.maxPts === 0) outOf -= gc.weight;
      }
      for (const gc of categories) {
        if (gc.maxPts !== 0) {
          totPer += ((gc.weight / outOf) * gc.totPts) / gc.maxPts;
        }
      }
      calcGrade = totPer;
    } else {
      let totPts = 0,
        maxPts = 0;
      for (const gc of categories) {
        totPts += gc.totPts;
        maxPts += gc.maxPts;
      }
      calcGrade = totPts / maxPts;
    }
    for (let i = 0; i < s.grading_category.length; i++) {
      const points =
        categories[i].div.firstElementChild.getElementsByClassName(
          "categorypoints"
        )[0] ?? document.createElement("div");
      const percent =
        categories[i].div.firstElementChild.getElementsByClassName(
          "categorypercent"
        )[0] ?? document.createElement("div");
      points.innerText =
        (Number.isInteger(categories[i].totPts)
          ? categories[i].totPts
          : categories[i].totPts.toFixed(2)) +
        "/" +
        (Number.isInteger(categories[i].maxPts)
          ? categories[i].maxPts
          : categories[i].maxPts.toFixed(2));
      percent.innerText = roundGrade(
        (categories[i].totPts / categories[i].maxPts) * 100
      );
      points.classList.add("categorypoints");
      percent.classList.add("categorypercent");
      categories[i].div.firstElementChild.appendChild(percent);
      categories[i].div.firstElementChild.appendChild(points);
    }
    document.getElementById("calculated-grade").innerText = roundGrade(
      calcGrade * 100
    );
  };

  const loadGrades = async () => {
    grades = [];
    courses = [];

    getContent("app-user-info")
      .then(async (v) => {
        const apiID = v.api_uid;
        getContent("users/" + apiID + "/grades").then(async (v) => {
          for (const s of v.section) {
            courses.push(s);
          }

          let request = `<?xml version="1.0" encoding="utf-8" ?><requests>`;
          for (const s of v.section) {
            request += "<request>/v1/sections/" + s.section_id + "</request>";
          }
          request += "</requests>";

          await multiGet(request)
            .then((r) => {
              for (let i = 0; i < r.response.length; i++) {
                const res = r.response[i];
                if (res.response_code === 403) continue;
                else {
                  courses[i].courseName =
                    res.body.course_title + " " + res.body.section_title;
                  grades.push({
                    courseName:
                      res.body.course_title + " " + res.body.section_title,
                    grade: roundGrade(v.section[i].final_grade[0].grade),
                    courseI: i,
                  });
                }
              }
              displayGrades();
            })
            .catch((e) => console.log(e));

          setTitle("Select a Course");
        });
      })
      .catch((e) => {
        if (e === 401) {
          authError();
        } else console.log(e);
      });
  };

  const loadAssignments = async (s) => {
    setTitle("Loading " + s.courseName + "...");
    const thisID = Math.floor(Math.random() * 1000000);
    assignmentShowID = thisID;
    assignments = [];
    assignmentNames = {};
    assignmentFactors = {};
    try {
      assignmentNames = GM_getValue("sg_assignment_names") ?? {};
      assignmentFactors = GM_getValue("sg_assignment_factors") ?? {};
    } catch (e) {}

    for (const p of s.period) {
      p.assignment.sort((a, b) => b.timestamp - a.timestamp);
      for (const a of p.assignment) {
        a.assignmentI = assignments.length;
        a.web_url = a.web_url?.replace("https://app", "https://" + domain);
        if (a.web_url && assignmentNames[a.web_url]) {
          assignments.push({
            grade: formatGrade(a),
            assignmentName: assignmentNames[a.web_url],
            url: a.web_url,
            category_id: a.category_id,
            assignmentI: a.assignmentI,
            factor: assignmentFactors[a.web_url] ?? 1,
            rawGrade: { pts: a.grade, max: a.max_points },
          });
          a.skip = true;
        } else {
          assignments.push({
            grade: formatGrade(a),
            assignmentName: "---",
            category_id: a.category_id,
            assignmentI: a.assignmentI,
            url: a.web_url,
            factor: assignmentFactors[a.web_url] ?? 1,
            rawGrade: { pts: a.grade, max: a.max_points },
          });
          a.skip = false;
        }
      }

      displayAssignments(s, thisID);

      const requests = [];
      for (let i = 0; i < p.assignment.length; i += 45) {
        requests.push(p.assignment.slice(i, i + 45));
      }
      for (const r of requests) {
        let request = `<?xml version="1.0" encoding="utf-8" ?><requests>`;
        const actualI = [];
        for (let i = 0; i < r.length; i++) {
          if (r[i].skip != true) {
            actualI.push(i);
            request += `<request>` + r[i].location + `</request>`;
          }
        }
        request += `</requests>`;
        if (actualI.length === 0) continue;

        const rereq = [];
        await multiGet(request)
          .then((v) => {
            for (let i = 0; i < v.response.length; i++) {
              let ai = actualI[i];
              const res = v.response[i];
              const url = r[ai].web_url;
              if (res.response_code === 403) {
                rereq.push({ assignmentI: r[ai].assignmentI, url: url });
              } else {
                assignments[r[ai].assignmentI].assignmentName = res.body.title;
                if (res.body.factor) {
                  assignments[r[ai].assignmentI].factor = parseFloat(
                    res.body.factor
                  );
                  assignmentFactors[url] = parseFloat(res.body.factor);
                }
                assignments[r[ai].assignmentI].url = url;
                assignmentNames[url] = res.body.title;
              }
            }
          })
          .catch((e) => console.log(e));

        displayAssignments(s, thisID);
        storeAssignmentDetails();
        await getAssignmentTitles(rereq, s, thisID);
      }
    }
    if (thisID == assignmentShowID) {
      setTitle(s.courseName);
    }
  };

  const getAssignmentTitles = (as, s, id, force = false) => {
    return new Promise(async (res, rej) => {
      if (as.length === 0) {
        res();
        return;
      }
      for (const a of as) {
        if (id !== assignmentShowID) break;
        if (assignmentNames[a.url] && !force) {
          assignments[a.assignmentI].assignmentName = assignmentNames[a.url];
          assignments[a.assignmentI].url = a.url;
        } else {
          await getHTML(a.url)
            .then((v) => {
              let i = v.search(/<title>/);
              let j = v.search(/<\/title>/);
              if (i === -1) {
                assignments[a.assignmentI].url = a.url;
              } else {
                const tmp = document.createElement("textarea");
                tmp.innerHTML = v.substring(i + 7, j - 12);

                assignments[a.assignmentI].assignmentName = tmp.value;
                assignmentNames[a.url] = tmp.value;
                assignments[a.assignmentI].url = a.url;
                storeAssignmentDetails();
                displayAssignments(s, id);
                tmp.remove();
              }
            })
            .catch((e) => {});
        }
      }
      storeAssignmentDetails();
      displayAssignments(s, id);
      res();
    });
  };

  start();
})();