Skip to content

Commit

Permalink
feat: begin creation of classes bookmarklet
Browse files Browse the repository at this point in the history
tl;dr: bookmarklet for utd's student portal that scrapes your schedule for the semester and creates an ical file to import into your calendar of choice
i just need to fix some date handling stuff and make a small page showing how to use, then its 🚢⌚!
  • Loading branch information
jasonappah committed Aug 22, 2022
1 parent 08b0fa7 commit cd38657
Show file tree
Hide file tree
Showing 7 changed files with 1,460 additions and 61 deletions.
2 changes: 1 addition & 1 deletion .eslintrc.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
"extends": ["next/core-web-vitals","wesbos/typescript"]
"extends": ["next/core-web-vitals", "wesbos/typescript"]
}
25 changes: 14 additions & 11 deletions next.config.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
/**
* @type {import('next').NextConfig}
*/
module.exports = {
async rewrites() {
return {
fallback: [
{
source: "/:path*",
destination: `/api/link/?slug=:path*`
}
]
}
}
}
rewrites() {
return {
fallback: [
{
source: '/:path*',
destination: `/api/link/?slug=:path*`,
},
],
};
}
};
10 changes: 7 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,19 @@
"scripts": {
"dev": "next dev",
"build": "next build",
"build:all": "yarn run build && yarn run build:bookmarklet",
"build:bookmarklet": "terser bookmarklet/main.js -o bookmarklet/main.min.js -c -m",
"start": "next start",
"lint": "next lint",
"lint:fix": "next lint --fix"
},
"dependencies": {
"airtable-plusplus": "^0.3.2",
"airtable-plusplus": "0.3.3",
"next": "^12.2.5",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"tailwindcss": "^3.1.8"
"tailwindcss": "^3.1.8",
"terser": "^5.14.2"
},
"devDependencies": {
"@babel/core": "^7.16.0",
Expand All @@ -39,6 +42,7 @@
"eslint-plugin-react-hooks": "^4.3.0",
"postcss": "^8.4.16",
"prettier": "^2.5.1",
"typescript": "^4.5.2"
"typescript": "^4.5.2",
"vercel": "^28.1.0"
}
}
11 changes: 5 additions & 6 deletions pages/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,17 @@ function Link({
return (
<a
href={href}
className={"text-accent hover:text-back hover:bg-content transition"}
className="transition text-accent hover:text-back hover:bg-content"
>
{children}
</a>
);
}

export const getStaticProps: GetStaticProps = () => {
export const getStaticProps: GetStaticProps<{ palette: string }> = () => {
const paletteOptions = Object.keys(styles);
const palette = paletteOptions[Math.floor(Math.random() * paletteOptions.length)];
const palette =
paletteOptions[Math.floor(Math.random() * paletteOptions.length)];
return {
props: {
palette,
Expand All @@ -41,7 +42,6 @@ function Index({ palette }: InferGetStaticPropsType<typeof getStaticProps>) {
const [electronicMailIdentifier, setEmail] = useState('...');
useEffect(() => {
setEmail(Buffer.from('aGV5QGphc29uYWEubWU=', 'base64').toString('utf8'));

console.log(
'👋 you should also follow my Instagram: https://instagram.com/jasonaa_'
);
Expand Down Expand Up @@ -74,14 +74,13 @@ function Index({ palette }: InferGetStaticPropsType<typeof getStaticProps>) {
✨ Learning and hacking with friends in{' '}
<Link href="https://hackclub.com">Hack Club</Link>
</li>

<li>
🎓 Completing a Computer Science degree at{' '}
<Link href="https://utdallas.edu">UT Dallas</Link>
</li>
</ul>
<p>
Outside of software engineering, I love music production 🎧,
Outside of software engineering, I love music production 🎹,
broadcast/audiovisual technology 📹, and all things technical
theatre 🎭&nbsp;!{' '}
<Link href="https://scrapbook.hackclub.com/jasonaa">
Expand Down
220 changes: 220 additions & 0 deletions public/bookmarklet/main.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
// https://dacs-prd.utshare.utsystem.edu/psc/DACSPRD/EMPLOYEE/SA/c/NUI_FRAMEWORK.PT_AGSTARTPAGE_NUI.GBL?CONTEXTIDPARAMS=TEMPLATE_ID%3aPTPPNAVCOL&scname=ADMN_MANAGE_CLASSES&PTPPB_GROUPLET_ID=UTD_SR_MANAGE_CLASSES&CRefName=ADMN_NAVCOLL_22
(() => {
// Runs on UTD Student Center > Manage My Classes > View My Classes
const parseEndTime = (time) => {
const end = time.split('-')[1].trim()
return new Date(end)
}

function getEndDate() {
const tables = Array.from(document.getElementsByTagName("iframe")).map(iframe => Array.from(iframe.contentDocument.getElementsByTagName("table"))).flat()
console.log(tables)
const classesTable = tables.find(table => table.id == "ACE_STDNT_ENRL_SSV2$0")
const classes = classesTable.firstElementChild.children;
let latestTime = new Date(0);
for (const c of classes) {
if (c.children.length === 1) continue;
const content = c.children;
if (content.length === 1) continue;
const possibleClass = content[1]
if (possibleClass && possibleClass.children !== 0) {
const yeah = possibleClass.firstElementChild
if (!yeah) continue
const [classNameEl, classDetailsEl] = yeah.firstElementChild.firstElementChild.children
// will any amount of refactoring save us from this awful, awful markup? hope so, this is the worst code I've ever written. theres maybe at least a way to make this more readable.
const sessions = Array.from(classDetailsEl.firstElementChild.firstElementChild.firstElementChild.children[2].children[1].firstElementChild.children[1].firstElementChild.firstElementChild.firstElementChild.firstElementChild.firstElementChild.children).slice(1)
for (const s of sessions) {
const t = parseEndTime(s.children[7].innerText)
if (t > latestTime) {
latestTime = t
}
}
}
}
return latestTime
}

// Runs on UTD Student Center > Manage My Classes > View My Weekly Schedule
const goToNextWeek = () => { getElementById("DERIVED_CLASS_S_SSR_NEXT_WEEK").click() }
const calcHops = (endDate) => {
let hops = 0;
let idk = new Date(getElementById("win0divDERIVED_CLASS_S_DESCR100_2", "div").firstElementChild.firstElementChild.firstElementChild.innerText.slice(-10).trim())
while (idk < endDate) {
hops++
idk.setDate(idk.getDate() + 7)
}
return hops

}
const maxTimestamp = 8640000000000000;
const getElementById = (id, tagName = "") => {
let el = document.getElementById(id)
if (el) return el
for (const frame of document.getElementsByTagName("iframe")) {
el = frame.contentDocument.getElementById(id)
if (el) return el
if (tagName) {
els = frame.contentDocument.getElementsByTagName(tagName)
if (els.length == 0) return el
for (const el of els) {
if (el.id == id) {
return el
}
}
}
}
return el
}
const DOMSettled = (run) => {
const begin = performance.now();
const timeout = 10000
const main = (whenComplete, reject) => {
let lastEvent = maxTimestamp
const observer = new MutationObserver(() => {
lastEvent = performance.now();
})
const wait = (start) => {
const waitTime = 2500
const now = performance.now()
if (lastEvent != maxTimestamp && now - lastEvent > waitTime) {
console.log(`DOM settled after ${performance.now() - begin}ms`)
observer.disconnect()
whenComplete()
return
}
if (performance.now() - start > timeout) {
if (lastEvent == maxTimestamp) {
console.log(`DOM was not mutated in ${timeout}ms, continuing...`)
observer.disconnect()
whenComplete()
return
}
reject("DOM not settled")
} else {
requestAnimationFrame(() => wait(start))
}
}
observer.observe(document, { attributes: true, childList: true, subtree: true })
for (const iframe of document.getElementsByTagName("iframe")) {
observer.observe(iframe, { attributes: true, childList: true, subtree: true })
}

run()
wait(performance.now())
}

return new Promise((resolve, reject) => {
main(resolve, reject)
})

}

const clickMenuButton = async (index) => {
let menu = document.getElementsByTagName("ul")[2]
const el = menu.children[index].children[1].firstElementChild
console.log("ABOUT TO CLICK")
await DOMSettled(() => { el.click(); console.log("Clicked") })
console.log("DOM DONE!")
}



const textExpectedInEl = (el, arr) => {
for (const text of arr) {
if (!el.innerText.includes(text)) return false
}
return true
}

const jsDateToIcalDate = (date) => {
return date.toISOString().match(/(\d)|([TZ])/g).join("")
}

const event = (courseNum, start, end, location, description) => {
return `\nBEGIN:VEVENT\nSUMMARY:${courseNum}\nDTSTART:${jsDateToIcalDate(start)}Z\nDTEND:${jsDateToIcalDate(end)}Z\nGEO:${location}\nDESCRIPTION:${description}\nEND:VEVENT`
}


const timeToObject = (time) => {
let obj = time.match(/(\d{1,2})/g)
let [hour, minute] = obj.map((str)=>Number(str))
if (time.includes("PM")) hour += 12
return [
hour, minute
]
}
const run = async () => {
if (!textExpectedInEl(document.body, ["Manage My Classes", "View My Weekly Schedule"])) return alert("Navigate to Orion > UTD Student Center > Manage My Classes > View My Classes, then rerun to continue.")
if (!confirm("Press OK to begin.")) return
await clickMenuButton(0)
const endDate = getEndDate()
console.log(endDate)
await clickMenuButton(1)
const termWeeks = calcHops(endDate)
getElementById("DERIVED_CLASS_S_SSR_DISP_TITLE", "input").click()
getElementById("DERIVED_CLASS_S_SHOW_INSTR", "input").click()
const refreshCalEl = getElementById("DERIVED_CLASS_S_SSR_REFRESH_CAL$38$", "input")
await DOMSettled(() => refreshCalEl.click())
let ics = "BEGIN:VCALENDAR\nVERSION:2.0\nPRODID:v2.jasonaa.me/bookmarklet"
for (let currentWeek = 0; currentWeek < termWeeks; currentWeek++) {
const tableBody = getElementById("trSSR_DUMMY_REC$0_row1").firstElementChild.firstElementChild.children[1].firstElementChild.children[2]
// console.log(tableBody.firstElementChild)
// first date in the table
// skip first row - it's the header
for (let timeSlot = 1; timeSlot < tableBody.children.length; timeSlot++) {
// console.log(tableBody.firstElementChild.children)
const thing = tableBody.children[timeSlot]
// skip first column - it is the time
for (let column = 1; column < thing.children.length; column++) {
// console.log("WOAHhHO",tableBody.firstElementChild.children[column])
const beginning = new Date(`${tableBody.firstElementChild.children[column].childNodes[2].textContent} 2022`)
beginning.setHours(7)
const columnEl = thing.children[column]
// console.log("YUH", columnEl)
if (columnEl.children.length === 0) continue

// const td = columnEl.firstElementChild
// const span = td.firstElementChild
const span = columnEl.firstElementChild

// console.log("OWO", span)
if (span.tagName !== "SPAN") continue
// console.log("POGGIER")

// console.log("vibing")
const nodes = span.childNodes
console.log(nodes)
const startDateTime = new Date(beginning)
// skill issue
const [startHour, startMinute] = timeToObject(nodes[6].textContent.substring(0,7).trim())
const [endHour, endMinute] = timeToObject(nodes[6].textContent.slice(-7).trim())
console.log({startHour, startMinute, endHour, endMinute})
startDateTime.setHours(startHour, startMinute)
const endDateTime = new Date(beginning)
endDateTime.setHours(endHour, endMinute)
// endDate.setHours(endTime.getHours())
// endDate.setMinutes(endTime.getMinutes())
console.log({ beginning, timeSlot, startDateTime, endDateTime })
ics += event(nodes[0].textContent, startDateTime, endDateTime, nodes[8].textContent, `${nodes[2].textContent} with ${nodes[12].textContent}`)
}
}
await DOMSettled(goToNextWeek)
}
ics += "\nEND:VCALENDAR"
const element = document.createElement("a")
element.setAttribute(
"href",
"data:text/calendar;charset=utf-8," +
encodeURIComponent(ics)
)
element.setAttribute("download", `term_${new Date().toISOString()}.ics`)
element.click()
}

try { run() } catch (e) {
alert("An unexpected error occurred.");
console.error(e)
}
})()


1 change: 1 addition & 0 deletions public/bookmarklet/main.min.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit cd38657

Please sign in to comment.