diff --git a/Poster.pdf b/Poster.pdf new file mode 100644 index 0000000..100db46 Binary files /dev/null and b/Poster.pdf differ diff --git a/README.md b/README.md index 71b6719..dcae785 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ [![CodeQL](https://github.com/AAP9002/Timetable-ICS-Live-Editor/actions/workflows/github-code-scanning/codeql/badge.svg)](https://github.com/AAP9002/Timetable-ICS-Live-Editor/actions/workflows/github-code-scanning/codeql) [![.github/workflows/deploy.yml](https://github.com/AAP9002/Timetable-ICS-Live-Editor/actions/workflows/deploy.yml/badge.svg)](https://github.com/AAP9002/Timetable-ICS-Live-Editor/actions/workflows/deploy.yml) +[![GitHub last commit (branch)](https://img.shields.io/github/last-commit/AAP9002/Timetable-ICS-Live-Editor/main?label=Last%20Live%20Publish&logo=iterm2)](https://github.com/AAP9002/Timetable-ICS-Live-Editor/) +[![Website](https://img.shields.io/website?url=https%3A%2F%2Faap9002.github.io%2FTimetable-ICS-Live-Editor%2F&logo=githubpages)](https://aap9002.github.io/Timetable-ICS-Live-Editor/) ![Screenshot from 2023-09-30 11-49-35](https://github.com/AAP9002/Timetable-ICS-Live-Editor/assets/42409957/390a7f9b-e74f-4b9f-89a6-818cba1577e7) @@ -29,3 +31,14 @@ Get started at [https://aap9002.github.io/Timetable-ICS-Live-Editor/](https://aa - [x] Materials Science - [x] Areospace Engineering - [x] Physics + +# Contributing +## Adding courses +Add courses in allCourses.md +## Adding features +1. Create an issue +1. fork the project +1. create a new branch +1. read ```/server/features/_new_feature_doc.md``` +1. add your feature +1. create a pull request \ No newline at end of file diff --git a/server/allCourses.md b/allCourses.md similarity index 100% rename from server/allCourses.md rename to allCourses.md diff --git a/client/src/App.js b/client/src/App.js index 7803e6d..6c2a715 100644 --- a/client/src/App.js +++ b/client/src/App.js @@ -75,10 +75,10 @@ function App() {

PRs are welcome, please feel free to add courses, add a feature or fix a bug

Github Repo: https://github.com/AAP9002/Timetable-ICS-Live-Editor/


- Contribute by adding courses here + Contribute by adding courses here

Supported Courses

-

T-I-L-E is available for a limited number of courses at UoM, you can add yours on github.

+

T-I-L-E is available for a limited number of courses at UoM, you can add yours on github.

So far, we support:

Computer Science Microbiology diff --git a/server/features/_new_feature_doc.md b/server/features/_new_feature_doc.md new file mode 100644 index 0000000..ba2e87c --- /dev/null +++ b/server/features/_new_feature_doc.md @@ -0,0 +1,29 @@ +## Implement your feature in its own file in /server/features +```js +// your feature +// @author : your github username +// @date : the date you created this file +// @description : a short description of what your feature does +// @params : any parameters your feature needs +// @returns : what your feature returns +// @notes : any notes you want to add + +function run(cal) { + // your code + + return cal +} + +module.exports = { + run, +}; +``` + +## Set up your feature in /server/index.js +- import your code within the commented section 'IMPORT FEATURES' +- Add a switch case for your feature in the performModifications method + +## Finally +- add your feature to the README.md +- create a pull request +- once merged, I will add code on the front end to users can enable your feature \ No newline at end of file diff --git a/server/features/forcedBreakPoint.js b/server/features/forcedBreakPoint.js new file mode 100644 index 0000000..891d3fa --- /dev/null +++ b/server/features/forcedBreakPoint.js @@ -0,0 +1,36 @@ +// Forcing calenders to update events once a day +// @author : aap9002 +// @date : 01/11/2023 +// @description : once a day between 01:00 and 04:00, force a refresh of the events to restyle any new formatting +// @params : the calender in string format +// @returns : the calender in string format +// @notes : Be aware the time is based on the server time not UTC + +const regex = /^LAST-MODIFIED:.*/gm; + +/** + * Force events to update format once a day + * @param {string} cal + * @returns cal with last modified date time set + */ +function run(cal) { + // once a day between 01:00 and 04:00, force a refresh of the events to restyle any new formatting + // this is as modifications to the event will not be recognised and updated unless the UoM event changes on the timetabling system itself + // so this will manual set the last modified each day to force an update in the calender app + // NB. it will stop doing this at 4 am so any changes in the day will be recognised and updated live + // NB. 3 hour window set as the ICS is set to refresh evert 2 hours, so this should affect all users + const date = new Date(); + const hour = date.getHours(); + if (hour >= 1 && hour <= 4) { + datestr = date.toISOString().split('T')[0]; + datestr = datestr.split('-').join(''); + datestr = "LAST-MODIFIED:" + datestr + "T000000"; + cal = cal.replace(regex, datestr); + } + + return cal; +} + +module.exports = { + run, +}; \ No newline at end of file diff --git a/server/features/replaceCodeName.js b/server/features/replaceCodeName.js new file mode 100644 index 0000000..b1f52aa --- /dev/null +++ b/server/features/replaceCodeName.js @@ -0,0 +1,81 @@ +// Replace course codes +// @author : aap9002 +// @date : 01/11/2023 +// @description : Find and replace course codes with [course names] or [course code and course name] +// @params : the calender in string format +// @returns : the calender in string format + + +const pattern = /SUMMARY:[^\/]*\//g // REGEX to search the ICAL for the course code + +/** + * List unique course names in the string + * @param {string} cal + * @returns List of unique course codes + */ +function parseCourseCodes(cal) { + const uniqueMatches = new Set(); + + let match; + while ((match = pattern.exec(cal)) !== null) { + //console.log(match[0].split(':')[1].split('/')[0]); + uniqueMatches.add(match[0].split(':')[1].split('/')[0]); + } + const uniqueCourseCodesArray = Array.from(uniqueMatches); + return uniqueCourseCodesArray; +} + +/** + * Replace course codes with full course names + * @param {string} cal + * @returns cal with replacements + */ +function replaceCourseCodesWithNames(cal, courses) { + // get unique course codes + let uniqueCourseCodesArray = parseCourseCodes(cal); + + // replace course codes with names + for (let i = 0; i < uniqueCourseCodesArray.length; i++) { + const courseCode = uniqueCourseCodesArray[i]; + try { + const courseName = courses.find(course => course.split(' ')[0] === courseCode).split(' ').slice(1).join(' '); + cal = cal.split(courseCode).join(courseName); + } + catch (e) { + // if the course code is not found in the allCourses.md file, log it + console.log(courseCode + " not found in allCourses.md"); + } + } + + return cal; +} + +/** + * Replace course codes with code and full course names + * @param {string} cal + * @returns cal with replacements + */ +function replaceCourseCodesWithCodeAndNames(cal, courses) { + // get unique course codes + let uniqueCourseCodesArray = parseCourseCodes(cal); + + // replace course codes with names + for (let i = 0; i < uniqueCourseCodesArray.length; i++) { + const courseCode = uniqueCourseCodesArray[i]; + try { + const courseName = courses.find(course => course.split(' ')[0] === courseCode).split(' ').slice(1).join(' '); + cal = cal.split(courseCode).join(courseCode + " " + courseName); + } + catch (e) { + // if the course code is not found in the allCourses.md file, log it + console.log(courseCode + " not found in allCourses.md"); + } + } + + return cal; +} + +module.exports = { + replaceCourseCodesWithNames, + replaceCourseCodesWithCodeAndNames +}; \ No newline at end of file diff --git a/server/index.js b/server/index.js index 8b7f79c..b0f120f 100644 --- a/server/index.js +++ b/server/index.js @@ -2,16 +2,67 @@ const express = require('express'); const app = express(); const axios = require('axios'); var fs = require('fs'); -const port = process.env.PORT || 5000; + +/////////////////////////////////////////////// IMPORT FEATURES //////////////////////////////////////////////// + +const syncForcedBreakpoint = require('./features/forcedBreakPoint.js') +const replaceTitle = require('./features/replaceCodeName.js') + +///////////////////////////////////////////// IMPORT FEATURES END ////////////////////////////////////////////// + +///////////////////////////////////////////////// FEATURES ///////////////////////////////////////////////////// + +/** + * perform modification defined by the user in stepsString + * - example of stepsString would be "00-02" + * - each code (e.g. "00") corresponds to one feature + * - perform steps in order + * @param {string} cal + * @param {string} stepsString + * @returns modified cal + */ +function performModifications(cal, stepsString) { + + const steps = stepsString.split('-') + + for (let i = 0; i < steps.length; i++) { + let step = steps[i]; + console.log(step) + + switch (step) { + case "00": // replace course code with just course name + cal = replaceTitle.replaceCourseCodesWithNames(cal, courses); + break; + case "01": // replace course code with course code and course name + cal = replaceTitle.replaceCourseCodesWithCodeAndNames(cal, courses); + break; + case "02": // force restyling of calender every 24 hours + cal = syncForcedBreakpoint.run(cal); + break; + default: + console.log("step "+ step + " called but not defined in performModifications switch") + } + } + + return cal; +} + +/////////////////////////////////////////////// FEATURES END//////////////////////////////////////////////////// + +/////////////////////////////////////////////////// SETUP ///////////////////////////////////////////////////// // This displays message that the server running and listening to specified port +const port = process.env.PORT || 5000; app.listen(port, () => console.log(`Listening on port ${port}`)); +var courses = []; // Predefined courses from allCourses.md +const validUrlPATTERN = /^https:\/\/scientia-eu-v4-api-d3-02\.azurewebsites\.net\/\/api\/ical\/[0-9a-fA-F-]+\/[0-9a-fA-F-]+\/timetable\.ics$/g; // REGEX for valid uom ics uri + + // import courses from allCourses.md -var courses = []; try { - var courses = fs.readFileSync('allCourses.md', 'utf8').split('\n'); - // test courses are read correctly + var courses = fs.readFileSync('../allCourses.md', 'utf8').split('\n'); + //test courses are read correctly // for (let i = 0; i < courses.length; i++) { // console.log(courses[i]); // } @@ -19,83 +70,147 @@ try { console.log('Error:', e.stack); } -// regex to search the ICAL for the course code -const pattern = /SUMMARY:[^\/]*\//g +////////////////////////////////////////////////// SETUP END /////////////////////////////////////////////////// +/////////////////////////////////////////////////// API V1 ///////////////////////////////////////////////////// app.get('/api/v1/:uniqueAPI/tt.ics', function (req, res) { - const { uniqueAPI } = req.params; - - const apiUrlDec = decodeURIComponent(uniqueAPI) // Decoding a UoM Timetable URL encoded value console.log("V1 API hit") - console.log(apiUrlDec) + + // Decoding a UoM Timetable URL encoded value + const { uniqueAPI } = req.params; + const apiUrlDec = decodeURIComponent(uniqueAPI) + //console.log(apiUrlDec) if (testValidUrl(apiUrlDec)) { - let rebuild = "https://scientia-eu-v4-api-d3-02.azurewebsites.net//api/ical/"+apiUrlDec.split('/')[6]+"/"+apiUrlDec.split('/')[7]+"/timetable.ics"; + let rebuild = "https://scientia-eu-v4-api-d3-02.azurewebsites.net//api/ical/" + apiUrlDec.split('/')[6] + "/" + apiUrlDec.split('/')[7] + "/timetable.ics"; console.log(rebuild) - axios.get(rebuild) - .then(response => { - let cal = response.data; - // FInd all unique course codes in the ICS file - const uniqueMatches = new Set(); + try { + getTimetable(rebuild).then(cal => { + if (cal != null) { - let match; - while ((match = pattern.exec(cal)) !== null) { - //console.log(match[0].split(':')[1].split('/')[0]); - uniqueMatches.add(match[0].split(':')[1].split('/')[0]); - } - const uniqueCourseCodesArray = Array.from(uniqueMatches); - //console.log(uniqueCourseCodesArray); - - // for each unique course code, find the course name and replace the course code with the course name - for (let i = 0; i < uniqueCourseCodesArray.length; i++) { - const courseCode = uniqueCourseCodesArray[i]; - try { - const courseName = courses.find(course => course.split(' ')[0] === courseCode).split(' ').slice(1).join(' '); - cal = cal.split(courseCode).join(courseName); - } - catch (e) { - // if the course code is not found in the allCourses.md file, log it - console.log(courseCode + " not found in allCourses.md"); - } - } + // steps for default V1 modifications + let steps = "00-02" + cal = performModifications(cal, steps); - // once a day between 01:00 and 04:00, force a refresh of the events to restyle any new formatting - // this is as modifications to the event will not be recognised and updated unless the UoM event changes on the timetabling system itself - // so this will manual set the last modified each day to force an update in the calender app - // NB. it will stop doing this at 4 am so any changes in the day will be recognised and updated live - // NB. 3 hour window set as the ICS is set to refresh evert 2 hours, so this should affect all users - const date = new Date(); - const hour = date.getHours(); - if (hour >= 1 && hour <= 4) { - datestr = date.toISOString().split('T')[0]; - datestr = datestr.split('-').join(''); - datestr = "LAST-MODIFIED:" + datestr + "T000000"; - const regex = /^LAST-MODIFIED:.*/gm; - cal = cal.replace(regex, datestr); + res.writeHead(200, { + "Content-Type": "text/calendar", + "Content-Disposition": "attachment; filename=tt.ics" + }) + res.end(cal) // return response as download + } + else { + throw ('Calender not received') } - - //HTTP Head - res.writeHead(200, { - "Content-Type": "text/calendar", - "Content-Disposition": "attachment; filename=tt.ics" - }) - - res.end(cal) // return response as download }); + } + catch (e) { + console.log(e); + res.status(500).send("error") + } + } else { res.status(400).send("Invalid API URL") } }); -/* - Test for valid api url - - checks if the url is a valid UoM api url - Params: string url - return: bool if link is valid -*/ +////////////////////////////////////////////////// API V1 END /////////////////////////////////////////////////// + +/////////////////////////////////////////////////// API V2 ///////////////////////////////////////////////////// +app.get('/api/v2/:steps/:uniqueAPIPart1/:uniqueAPIPart2/tt.ics', function (req, res) { + console.log("V2 API hit") + + // Decoding a UoM Timetable URL encoded value + const { steps, uniqueAPIPart1, uniqueAPIPart2 } = req.params; + + if( !containsOnlyUUID(uniqueAPIPart1) || !containsOnlyUUID(uniqueAPIPart2)) + { + console.log("Invalid keys provided in API url") + res.status(403).send("Invalid keys provided in API url") + } + else{ + + let URL = "https://scientia-eu-v4-api-d3-02.azurewebsites.net//api/ical/" + uniqueAPIPart1 + "/" + uniqueAPIPart2 + "/timetable.ics"; + + try { + getTimetable(URL).then(cal => { + if (cal != null) { + + cal = performModifications(cal, steps) + + res.writeHead(200, { + "Content-Type": "text/calendar", + "Content-Disposition": "attachment; filename=tt.ics" + }) + res.end(cal) // return response as download + } + else { + throw ('Calender not received') + } + }); + } + catch (e) { + console.log(e); + res.status(500).send("error") + } + } +}); + +////////////////////////////////////////////////// API V2 END /////////////////////////////////////////////////// + +////////////////////////////////////////////// FUNDAMENTAL METHODS ////////////////////////////////////////////// +/** + * Test for valid api url + * - checks if the url is a valid UoM api url + * @param {string} user url + * @returns boolean if url valid + */ function testValidUrl(url) { - const validUrlPATTERN = /^https:\/\/scientia-eu-v4-api-d3-02\.azurewebsites\.net\/\/api\/ical\/[0-9a-fA-F-]+\/[0-9a-fA-F-]+\/timetable\.ics$/g; return validUrlPATTERN.test(url); -} \ No newline at end of file +} + +/** + * Testing the parts of the API key + * - checking there is only UUID + * - reduce chance of Server-side request forgery + * + * @param {string} inputString + * @returns Weather the string is a valid part + */ +function containsOnlyUUID(inputString) { + // Regular expression to match only UUID + const letterAndDashRegex = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/; + return letterAndDashRegex.test(inputString); +} + +/** + * Get remote timetable and return a string + * @param {string} timetableUri + * @returns string of remote calender contents + */ +async function getTimetable(timetableUri) { + try { + let response = await axios({ + url: timetableUri, + method: 'get', + timeout: 8000, + headers: { + 'Content-Type': 'application/json', + } + }); + + if (response.status == 200) { + return response.data; + } + else { + throw ('axios failed: status ' + response.status) + } + } + catch (e) { + //console.log(e); + return null; + } +} + +////////////////////////////////////////////// FUNDAMENTAL METHODS END ////////////////////////////////////////////// \ No newline at end of file