From 6bd6bd400ae5b3e6b2a946fd28192653b87cb5fe Mon Sep 17 00:00:00 2001 From: Tyler Hill Date: Wed, 11 Dec 2024 02:21:15 -0600 Subject: [PATCH] Switch to links for adding to gcal --- .env.chrome | 1 - .env.firefox | 1 - package.json | 8 +- src/background/index.ts | 91 ++++-------- src/content.ts | 297 +++++++++++++++++++--------------------- src/pages/index.tsx | 27 ---- 6 files changed, 172 insertions(+), 253 deletions(-) delete mode 100644 .env.chrome delete mode 100644 .env.firefox diff --git a/.env.chrome b/.env.chrome deleted file mode 100644 index ac94810..0000000 --- a/.env.chrome +++ /dev/null @@ -1 +0,0 @@ -CLIENT_ID=301819269847-vimdiv3n32jsf5m1fj880643n87cegi4.apps.googleusercontent.com diff --git a/.env.firefox b/.env.firefox deleted file mode 100644 index 67ec800..0000000 --- a/.env.firefox +++ /dev/null @@ -1 +0,0 @@ -CLIENT_ID= diff --git a/package.json b/package.json index e37bda4..ffd1913 100644 --- a/package.json +++ b/package.json @@ -92,12 +92,7 @@ } }, "manifest": { - "oauth2": { - "client_id": "$CLIENT_ID", - "scopes":["openid", "https://www.googleapis.com/auth/calendar.events"] - }, "permissions": [ - "identity", "storage", "scripting", "webNavigation", @@ -114,7 +109,6 @@ "id": "skedge@utdnebula", "strict_min_version": "109.0" } - }, - "key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvAmiiOsDH3dFqL26yZgZVRJd+NnnlA2l1x0K9HEFa3UgQ98xI+BOpE+NZuH6I7zSuA+vv+vwFnjj30pRTTUlsrDrrKLR80Rgs6/cZojz0o84E6EtWgAOx2g4BCwesvrJ51WoDz+kLOmSI29DNP/oYM0jstZlYuOvFswron9z48n12nhdg5KI9d0i5q0xYBgB8Hbmi2cKKPnW5urDIVez+D7GqIBXFAVMfas8FanTAqVvQ+c9UZiywauaolf+a1wia3D4pBQxztm/uWH23QIWFeHYxywmxZPKqYOBo1XyZYJAW4FaZgkUuMWSZXDUZk/oGELIVdQ8mzaXKDo+9ug97wIDAQAB" + } } } diff --git a/src/background/index.ts b/src/background/index.ts index 3886c39..a8a772e 100644 --- a/src/background/index.ts +++ b/src/background/index.ts @@ -1,12 +1,12 @@ import { Storage } from '@plasmohq/storage'; import { + addGCalButtons, type CourseHeader, listenForTableChange, scrapeCourseData, } from '~content'; import { neededOrigins } from '~data/config'; -// import { addGoogleOAuth } from '~popup'; export interface ShowCourseTabPayload { header: CourseHeader; @@ -24,14 +24,25 @@ const realBrowser = process.env.PLASMO_BROWSER === 'chrome' ? chrome : browser; /** Injects the content script if we hit a course page */ realBrowser.webNavigation.onHistoryStateUpdated.addListener((details) => { - if ( + const onOptions = /^.*:\/\/utdallas\.collegescheduler\.com\/terms\/.*\/courses\/.+$/.test( details.url, - ) || + ); + const onCurrentSchedule = /^.*:\/\/utdallas\.collegescheduler\.com\/terms\/.*\/currentschedule$/.test( details.url, - ) - ) { + ); + if (onOptions) { + //Listen for table change to rescrape data + realBrowser.tabs.sendMessage(details.tabId, 'disconnectObserver'); + realBrowser.scripting.executeScript({ + target: { + tabId: details.tabId, + }, + func: listenForTableChange, + }); + } + if (onOptions || onCurrentSchedule) { //Scrape data realBrowser.scripting.executeScript( { @@ -50,21 +61,25 @@ realBrowser.webNavigation.onHistoryStateUpdated.addListener((details) => { } }, ); - //Listen for table change to rescrape data - realBrowser.tabs.sendMessage(details.tabId, 'disconnectObserver'); - realBrowser.scripting.executeScript({ - target: { - tabId: details.tabId, - }, - func: listenForTableChange, - }); //Store tab info realBrowser.action.setBadgeText({ text: '!' }); realBrowser.action.setBadgeBackgroundColor({ color: 'green' }); courseTabId = details.tabId; storage.set('courseTabId', courseTabId); storage.set('courseTabUrl', details.url); - } else { + } + if (onCurrentSchedule) { + //Add GCal buttons + realBrowser.scripting.executeScript({ + target: { + tabId: details.tabId, + }, + // content script injection only works reliably on the prod packaged extension + // b/c of the plasmo dev server connections + func: addGCalButtons, + }); + } + if (!onOptions && !onCurrentSchedule) { realBrowser.action.setBadgeText({ text: '' }); } }); @@ -90,13 +105,6 @@ realBrowser.runtime.onMessage.addListener(function (message) { } }); -realBrowser.runtime.onMessage.addListener(function (message) { - if (message.name === 'insertEventToGoogleCalendar') { - console.log(message.event, message.token); - insertEventToGoogleCalendar(message.event); - } -}); - /** Sets the icon to be active if we're on a course tab */ realBrowser.tabs.onActivated.addListener(async () => { const cachedTabUrl: string = await storage.get('courseTabUrl'); @@ -143,44 +151,3 @@ async function getCurrentTab() { const [tab] = await realBrowser.tabs.query(queryOptions); return tab; } - -export async function insertEventToGoogleCalendar(event) { - console.log('added', event.pid, event.toString()); - - try { - chrome.identity.getAuthToken( - { - interactive: false, - }, - (token) => { - if (!token) { - chrome.identity.clearAllCachedAuthTokens(); - chrome.storage.local.set({}, function () {}); - } - chrome.storage.local.set({ token: token }, function () {}); - const headers = new Headers({ - Authorization: 'Bearer ' + token, - 'Content-Type': 'application/json', - }); - - const body = JSON.stringify(event); - - fetch( - 'https://www.googleapis.com/calendar/v3/calendars/primary/events', - { - method: 'POST', - headers: headers, - body: body, - }, - ) - .then((response) => response.json()) - .then((data) => console.log('Event added:', data)) - .catch((error) => { - console.error('Error adding event:', error); - }); - }, - ); - } catch (error) { - console.error(error); - } -} diff --git a/src/content.ts b/src/content.ts index f74c943..3ecfc5b 100644 --- a/src/content.ts +++ b/src/content.ts @@ -21,21 +21,6 @@ export const config: PlasmoCSConfig = { * - It injects the instructor names into the section table */ export async function scrapeCourseData() { - const semesters = { - S25: { - firstMonthOfSemester: '01', - firstMondayOfSemester: 20, - lastMonthOfSemester: '05', - lastFridayOfSemester: 15, - }, - F25: { - firstMonthOfSemester: '08', - firstMondayOfSemester: 18, - lastMonthOfSemester: '12', - lastFridayOfSemester: 5, - }, - }; - const [header, professors] = await Promise.all([ getCourseInfo(), injectAndGetProfessorNames(), @@ -75,40 +60,34 @@ export async function scrapeCourseData() { const courseRows = courseTable.querySelectorAll('tbody'); // add Professor header to the table - const tableHeaders = courseTable.querySelector('thead > tr'); const newHeader = document.createElement('th'); const line1 = document.createElement('div'); line1.innerText = 'Instructor(s)'; newHeader.append(line1); - - chrome.storage.local.get('token', async function (tokenStored) { - console.log(tokenStored); - if (typeof tokenStored.token !== 'undefined') { - // add Save to Google Calendar - const newHeader2 = document.createElement('th'); - const saveLine = document.createElement('div'); - saveLine.innerText = 'Save to \nGoogle Calendar'; - newHeader2.append(saveLine); - - // add Skedge reminder - const calLine2 = document.createElement('div'); - calLine2.style.fontWeight = 'normal'; - calLine2.style.paddingTop = '0.5rem'; - calLine2.innerText = 'From Skedge'; - newHeader2.append(calLine2); - tableHeaders.insertBefore(newHeader2, tableHeaders.children[1]); - } - }); // add Skedge reminder const line2 = document.createElement('div'); line2.style.fontWeight = 'normal'; line2.style.paddingTop = '0.5rem'; line2.innerText = 'From Skedge'; newHeader.append(line2); - tableHeaders.insertBefore(newHeader, tableHeaders.children[7]); + const tableHeaders = courseTable.querySelector('thead > tr'); + let sectionPlace; + for ( + sectionPlace = 0; + sectionPlace < tableHeaders.children.length; + sectionPlace++ + ) { + if ( + (tableHeaders.children[sectionPlace] as HTMLElement).innerText === + 'Section' + ) { + break; + } + } + sectionPlace++; + tableHeaders.insertBefore(newHeader, tableHeaders.children[sectionPlace]); courseRows.forEach((courseRow) => { - console.log('row'); // get professor name from course row const sectionDetailsButton = courseRow.querySelector('tr > td > button'); @@ -116,134 +95,26 @@ export async function scrapeCourseData() { sectionDetailsButton.click(); const sectionDetails = courseRow.querySelector('tr:nth-child(2)'); const sectionDetailsList = sectionDetails.querySelectorAll('li'); - let professor = ''; - let title = ''; + let professor; sectionDetailsList.forEach((li) => { const detailLabelText = li.querySelector('strong > span').innerText; if (detailLabelText.includes('Instructor')) { professor = li.innerText.split(':')[1].trim(); } - if (detailLabelText.includes('Description')) { - title = li.innerText.split(':')[1].split('(')[0].trim(); - } }); // append professor name to the table const newTd = document.createElement('td'); newTd.innerText = professor ?? 'No Instructor'; - const newButtonTd = document.createElement('td'); - const newButton = document.createElement('button'); - newButtonTd.appendChild(newButton); - newButton.style.background = '#E98300'; - newButton.style.color = '#000'; - newButton.style.border = 'none'; - newButton.style.borderRadius = '5px'; - newButton.style.padding = '10px'; - newButton.style.margin = '10px auto 10px auto'; - newButton.style.display = 'block'; - newButton.innerText = 'Add to Calendar'; - // this is in case we have multiple instructions per section - const sectionProfessors = professor.split(','); - sectionProfessors.forEach((sectionProfessor) => { - professors.push(sectionProfessor.trim()); - }); - const courseRowCells = courseRow.querySelector('tr'); - const times = - courseRowCells.children[courseRowCells.children.length - 1].textContent; - courseRowCells.insertBefore(newTd, courseRowCells.children[7]); - - const semester = semesters.S25; - - // parse - let days = times - .split(' ')[0] - .replace('M', 'MO,') - .replace('W', 'WE,') - .replace('F', 'FR,') - .replace('Th', 'TH,') - .replace('T', 'TU,'); - if (days[days.length - 1] == ',') { - days = days.slice(0, days.length - 1); + if (typeof professor !== 'undefined') { + // this is in case we have multiple instructions per section + const sectionProfessors = professor.split(','); + sectionProfessors.forEach((sectionProfessor) => { + professors.push(sectionProfessor.trim()); + }); } - - let day1 = semester.firstMondayOfSemester; - switch (days.slice(0, 2)) { - case 'MO': { - day1 = semester.firstMondayOfSemester; - break; - } - case 'TU': { - day1 = semester.firstMondayOfSemester + 1; - break; - } - case 'WE': { - day1 = semester.firstMondayOfSemester + 2; - break; - } - case 'TH': { - day1 = semester.firstMondayOfSemester + 2; - break; - } - case 'FR': { - day1 = semester.firstMondayOfSemester + 4; - break; - } - } - - const splitTimes = times.split(' '); - let startTime = splitTimes[1].replace('am', ''); - let endTime = splitTimes[3].replace('am', ''); - if (startTime.includes('pm')) { - startTime = startTime.replace('pm', ''); - const startTimeNum = Number(startTime.split(':')[0]); - startTime = - ( - Number(startTime.split(':')[0]) + (startTimeNum !== 12 ? 12 : 0) - ).toString() + - ':' + - startTime.split(':')[1]; - } - if (endTime.includes('pm')) { - endTime = endTime.replace('pm', ''); - const endTimeNum = Number(endTime.split(':')[0]); - endTime = - ( - Number(endTime.split(':')[0]) + (endTimeNum !== 12 ? 12 : 0) - ).toString() + - ':' + - endTime.split(':')[1]; - } - const event1 = { - summary: title, - organization: 'Class from Skedge', - start: { - dateTime: `2025-${semester.firstMonthOfSemester}-${day1}T${startTime}:00-06:00`, - timeZone: 'America/Chicago', - }, - end: { - dateTime: `2025-${semester.firstMonthOfSemester}-${day1}T${endTime}:00-06:00`, - timeZone: 'America/Chicago', - }, - recurrence: [ - `RRULE:FREQ=WEEKLY;UNTIL=2025${semester.lastMonthOfSemester}${semester.lastFridayOfSemester}T170000Z;BYDAY=${days}`, - ], - pid: 0, - }; - - chrome.storage.local.get('token', async function (tokenStored) { - if (typeof tokenStored.token !== 'undefined') { - newButton.onclick = async () => { - chrome.runtime.sendMessage({ - name: 'insertEventToGoogleCalendar', - event: event1, - token: tokenStored.token, - }); - alert(`Added ${event1.summary} to calendar.`); - }; - courseRowCells.insertBefore(newButton, courseRowCells.children[1]); - } - }); - + const courseRowCells = courseRow.querySelector('tr'); + courseRowCells.insertBefore(newTd, courseRowCells.children[sectionPlace]); //Increase Disabled Reasons row colspan if necessary const sectionDisabled = courseRow.querySelector( 'tr:nth-child(3) > td', @@ -260,7 +131,8 @@ export async function scrapeCourseData() { /** This listens for clicks on the buttons that switch between the enabled and disabled professor tabs and reports back to background.ts */ export function listenForTableChange() { - const realBrowser = process.env.PLASMO_BROWSER === 'chrome' ? chrome : browser; + const realBrowser = + process.env.PLASMO_BROWSER === 'chrome' ? chrome : browser; const observer = new MutationObserver((mutationsList) => { for (const mutation of mutationsList) { if ( @@ -286,3 +158,118 @@ export function listenForTableChange() { } }); } + +export async function addGCalButtons() { + /** Gets the first element from the DOM specified by selector */ + function waitForElement(selector: string): Promise { + return new Promise((resolve) => { + if (document.querySelector(selector)) { + return resolve(document.querySelector(selector)); + } + const observer = new MutationObserver(() => { + if (document.querySelector(selector)) { + resolve(document.querySelector(selector)); + observer.disconnect(); + } + }); + observer.observe(document.body, { + childList: true, + subtree: true, + }); + }); + } + + const courseTable = await waitForElement('table'); + + // add Save to Google Calendar + const newHeader = document.createElement('th'); + const line1 = document.createElement('div'); + line1.innerText = 'Save to \nGoogle Calendar'; + newHeader.append(line1); + // add Skedge reminder + const line2 = document.createElement('div'); + line2.style.fontWeight = 'normal'; + line2.style.paddingTop = '0.5rem'; + line2.innerText = 'From Skedge'; + newHeader.append(line2); + const tableHeaders = courseTable.querySelector('thead > tr'); + tableHeaders.insertBefore(newHeader, tableHeaders.children[1]); + + const courseRows = courseTable.querySelectorAll('tbody'); + const newTds = []; + courseRows.forEach((courseRow) => { + const newTd = document.createElement('td'); + newTds.push(newTd); + const courseRowCells = courseRow.querySelector('tr'); + courseRowCells.insertBefore(newTd, courseRowCells.children[1]); + }); + + let courses = await fetch( + 'https://utdallas.collegescheduler.com/api/term-data/2025%20Spring', + ); + courses = (await courses.json()).currentSections; + + if (typeof courses !== 'undefined') { + for (let i = 0; i <= newTds.length; i++) { + // append button to the table + const newTd = newTds[i]; + const courseData = courses[i]; + const links = []; // each metting + for (let j = 0; j < courseData.meetings.length; j++) { + const meeting = courseData.meetings[j]; + const OFFSET_HOURS = 6; // time zone fix + const formatTime = (date, time) => { + const datePart = new Date(date); + const timePart = String(time).padStart(4, '0'); + const hours = parseInt(timePart.slice(0, 2), 10); + const minutes = parseInt(timePart.slice(2), 10); + datePart.setUTCHours(hours + OFFSET_HOURS, minutes, 0, 0); + return `${datePart.toISOString().replace(/[-:]/g, '').split('.')[0]}Z`; + }; + const formattedStartDate = formatTime( + meeting.startDate, + meeting.startTime, + ); + const formattedEndTime = formatTime(meeting.startDate, meeting.endTime); + const recurrenceEnd = + meeting.endDate.split('T')[0].replaceAll('-', '') + 'T235959Z'; + const meetingDays = meeting.days + .replaceAll('Th', 'X') + .split('') + .map( + (letter) => + ({ M: 'MO', T: 'TU', W: 'WE', X: 'TH', F: 'FR' })[letter], + ) + .join(','); + const recurrence = `RRULE:FREQ=WEEKLY;UNTIL=${recurrenceEnd};BYDAY=${meetingDays}`; + links.push( + `https://calendar.google.com/calendar/r/eventedit?text=${courseData.subject} ${courseData.course}&dates=${formattedStartDate}/${formattedEndTime}&location=${meeting.building}&recur=${recurrence}`, + ); + } + // make a button to open multiple links at once when necessaary + let newLink; + if (links.length > 1) { + newLink = document.createElement('button'); + newLink.innerText = 'Add to Calendar (' + links.length + ')'; + newLink.onclick = function () { + for (const link of links) { + window.open(link); + } + }; + } else { + newLink = document.createElement('a'); + newLink.innerText = 'Add to Calendar'; + newLink.target = '_blank'; + newLink.href = links[0]; + } + newLink.style.background = '#E98300'; + newLink.style.color = '#000'; + newLink.style.border = 'none'; + newLink.style.borderRadius = '4px'; + newLink.style.padding = '6px 12px'; + newLink.style.margin = '10px auto'; + newLink.style.display = 'block'; + newTd.appendChild(newLink); + } + } +} diff --git a/src/pages/index.tsx b/src/pages/index.tsx index 4880a3a..d4d2764 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -1,4 +1,3 @@ -import { Button } from '@mui/material'; import { sendToBackground } from '@plasmohq/messaging'; import React, { useEffect, useState } from 'react'; @@ -14,7 +13,6 @@ import fetchWithCache, { cacheIndexGrades, expireTime, } from '~data/fetchWithCache'; -import { addGoogleOAuth } from '~popup'; import { convertToProfOnly, type SearchQuery, @@ -177,19 +175,6 @@ function removeDuplicates(array: SearchQuery[]) { const Index = () => { const [page, setPage] = useState<'landing' | 'list' | SearchQuery>('landing'); - const [isSignedIn, setIsSignedIn] = useState(false); - useEffect(() => { - if (process.env.PLASMO_BROWSER === 'chrome') { - chrome.storage.local.get('token', (tokenStored) => { - if (!tokenStored.token) { - setIsSignedIn(false); - } else { - console.log(tokenStored.token); - setIsSignedIn(true); - } - }); - } - }); const [listScroll, setListScroll] = useState(0); function setPageAndScroll(set: 'landing' | 'list' | SearchQuery) { if (set === 'list') { @@ -343,18 +328,6 @@ const Index = () => { rmp={rmp} setPage={setPageAndScroll} /> - {process.env.PLASMO_BROWSER === 'chrome' && !isSignedIn && ( - - )} {page !== 'list' && (