diff options
| -rw-r--r-- | README.md | 11 | ||||
| -rw-r--r-- | config-example.json | 9 | ||||
| -rw-r--r-- | package.json | 1 | ||||
| -rw-r--r-- | src/core/IEvent.ts | 7 | ||||
| -rw-r--r-- | src/index.ts | 48 | ||||
| -rw-r--r-- | src/sinks/GoogleCalendarSink.ts | 100 | ||||
| -rw-r--r-- | src/sinks/ISink.ts | 7 | ||||
| -rw-r--r-- | src/sinks/IcalSink.ts | 46 |
8 files changed, 30 insertions, 199 deletions
@@ -8,13 +8,4 @@ The [Ostfalia SPlus lecture schedule](http://splus.ostfalia.de) is complicated t Run `npm install` to install the necessary dependencies. -### Google - -Follow the steps provided by Google: https://developers.google.com/calendar/auth#OAuth2Authorizing -You will receive a `client_secret.json` file which contains your Google API credentials. Save this file as `etc/google/client_secret.json`. - -Adjust `config.ts` so that it uses the `sinks/GoogleCalendarSink`. Then run `npm start` in a terminal, visit the printed link and paste the code that will be provided there into the terminal window. - -### ICS - -Adjust `config.ts` so that it uses the `sinks/IcalSink`. Run `npm start` to generate the calendar file. +Copy and adjust `config-example.json` and run `npm start config.json`. diff --git a/config-example.json b/config-example.json index 62f703d..6e4a5c5 100644 --- a/config-example.json +++ b/config-example.json @@ -3,12 +3,7 @@ "icsPath": "kalender/informatik1.ics", "prefetchWeeks": 4, - "titleWhitelist": [ - "Programmieren", - "Datenbanken", - "Diskrete Strukturen", - "Reservierung", - "Einführung", - "Grundlagen des Programmierens" + "titleBlacklist": [ + "Programmieren" ] } diff --git a/package.json b/package.json index 3305dd4..e822012 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,6 @@ "dependencies": { "bluebird": "^3.5.1", "cheerio": "^1.0.0-rc.2", - "googleapis": "^27.0.0", "ical-generator": "^0.2.10", "moment": "^2.22.1", "request": "^2.85.0", diff --git a/src/core/IEvent.ts b/src/core/IEvent.ts deleted file mode 100644 index 6ce817d..0000000 --- a/src/core/IEvent.ts +++ /dev/null @@ -1,7 +0,0 @@ -export interface IEvent { - summary: string; - description: string; - location: string; - start: Date; - end: Date; -} diff --git a/src/index.ts b/src/index.ts index 3d7e588..bb3b6c7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,57 +1,63 @@ import * as moment from 'moment'; import * as BPromise from 'bluebird'; import * as fs from 'fs'; +import * as ical from 'ical-generator'; + +import {createHash} from 'crypto'; import {SplusApi} from './core/SplusApi'; -import {IEvent} from './core/IEvent'; import {ILecture} from './core/ILecture'; -import {IcalSink} from './sinks/IcalSink'; -import {GoogleCalendarSink} from './sinks/GoogleCalendarSink'; +const sha256 = (x) => createHash('sha256').update(x, 'utf8').digest('hex'); +const flatten = (arr) => [].concat(...arr); const range = (upper: number): number[] => Array.from(Array(upper), (x, i) => i); -const xprod = (arr1, arr2) => [].concat(...arr1.map((e1) => arr2.map((e2) => [e1, e2]))); +const xprod = (arr1, arr2) => flatten(arr1.map((e1) => arr2.map((e2) => [e1, e2]))); const isIncluded = (s1) => (s2) => s1.includes(s2); const anyTrue = (arr, cond) => arr.filter(cond).length > 0; +const allFalse = (arr, cond) => !anyTrue(arr, cond); +const uniqueByKey = (arrOfObj, key) => flatten((new Map(arrOfObj.map(obj => [obj[key], obj]))).values()); async function main(configPath) { const config = JSON.parse(fs.readFileSync(configPath, 'utf8')); - const sink = config.icsPath == undefined? new GoogleCalendarSink() : new IcalSink(config.icsPath); - const baseDate = moment().startOf('week'); - await BPromise.map(xprod(config.courses, range(config.prefetchWeeks)), async ([course, weeksAhead]) => { - const unfilteredLectures = await SplusApi.getData(course, baseDate.weeks() + weeksAhead); - const lectures = unfilteredLectures.filter(lecture => anyTrue(config.titleWhitelist, isIncluded(lecture.title))); + const events = flatten(await BPromise.map(xprod(config.courses, range(config.prefetchWeeks)), async ([course, weeksAhead]) => { + const week = baseDate.clone().add(weeksAhead, 'weeks').weeks(); + const unfilteredLectures = await SplusApi.getData(course, week); + const lectures = unfilteredLectures.filter(lecture => allFalse(config.titleBlacklist, isIncluded(lecture.title))); + console.log(`fetching course ${course} week ${week}, ${lectures.length} lectures found`); - await BPromise.map(lectures, async (lec: ILecture) => { - let beginDate = baseDate.clone(); + return lectures.map((lec: ILecture) => { + const beginDate = baseDate.clone(); beginDate.add(moment.duration({ days: lec.day + 1, // for moment: 1 = Monday hours: lec.begin, weeks: weeksAhead, })); - let endDate = baseDate.clone(); + const endDate = baseDate.clone(); endDate.add(moment.duration({ days: lec.day + 1, hours: lec.end, weeks: weeksAhead, })); - const event: IEvent = { + + const uid = sha256(JSON.stringify({ lec, beginDate, endDate })).substr(0, 16); + return { + uid, + start: beginDate.toDate(), + end: endDate.toDate(), + timestamp: moment().toDate(), summary: lec.title, description: lec.lecturer + (lec.info !== '' ? ' - ' : '') + lec.info, location: lec.room, - start: beginDate.toDate(), - end: endDate.toDate(), }; - - await sink.createEvent(event); - - console.log(lec.title, beginDate.toISOString(), endDate.toISOString()); }); - }); + }, { concurrency: 1 })); // splus session breaks when sending multiple concurrent calls - await sink.commit(); + // ref. https://www.npmjs.com/package/ical-generator + const cal = ical({ events: uniqueByKey(events, 'uid') }).toString(); + fs.writeFileSync(config.icsPath, cal.toString()); } main(process.argv[2] || './config-example.json').catch(console.log); diff --git a/src/sinks/GoogleCalendarSink.ts b/src/sinks/GoogleCalendarSink.ts deleted file mode 100644 index 374cc48..0000000 --- a/src/sinks/GoogleCalendarSink.ts +++ /dev/null @@ -1,100 +0,0 @@ -import * as fs from 'fs'; -import * as readline from 'readline'; - -import {google} from 'googleapis'; - -import {ISink} from './ISink'; -import {IEvent} from '../core/IEvent'; - - -const OAuth2Client = google.auth.OAuth2; -const scopes = ['https://www.googleapis.com/auth/calendar']; - -const credentialsPath = 'etc/google/client_secret.json'; -const tokenPath = 'etc/google/credentials.json'; - -const clientPromise = new Promise((resolve, reject) => { - fs.readFile(credentialsPath, (err, data) => { - if (err) return reject(err); - - const credentials = JSON.parse(data.toString()); - const {client_secret, client_id, redirect_uris} = credentials.installed; - - const client = new OAuth2Client(client_id, client_secret, redirect_uris[0]); - - fs.readFile(tokenPath, (err, data) => { - if (err) { - const authUrl = client.generateAuthUrl({ - access_type: 'offline', - scope: scopes, - }); - - console.log('Authorize this app by visiting this url:', authUrl); - - const rl = readline.createInterface({ - input: process.stdin, - output: process.stdout, - }); - - rl.question('Enter the code from that page here: ', code => { - rl.close(); - client.getToken(code, (err, token) => { - if (err) return reject(err); - fs.writeFile(tokenPath, JSON.stringify(token), err => err ? console.error(err) : 0); - client.setCredentials(token); - resolve(client); - }); - }); - } - else { - client.setCredentials(JSON.parse(data.toString())); - resolve(client); - } - }); - }); -}); - - -const calendarPromise = clientPromise.then(auth => google.calendar({version: 'v3', auth})); - - -export class GoogleCalendarSink implements ISink { - private _calendarId: string; - - constructor(calendarId = 'primary') { - this._calendarId = calendarId; - } - - createEvent(event: IEvent): Promise<void> { - const googleEvent = { - summary: event.summary, - description: event.description, - location: event.location, - start: { - dateTime: event.start.toISOString() - }, - end: { - dateTime: event.end.toISOString() - }, - reminders: { - useDefault: false - } - }; - - return calendarPromise.then(calendar => { - return new Promise<void>((resolve, reject) => { - calendar.events.insert({ - calendarId: this._calendarId, - resource: googleEvent, - }, function (err, event) { - if (err) return reject(err); - resolve(); - }); - }); - }); - } - - commit(): Promise<void> { - return Promise.resolve(); - } -} diff --git a/src/sinks/ISink.ts b/src/sinks/ISink.ts deleted file mode 100644 index aaacf35..0000000 --- a/src/sinks/ISink.ts +++ /dev/null @@ -1,7 +0,0 @@ -import {IEvent} from '../core/IEvent'; - -export interface ISink { - createEvent(event: IEvent): Promise<void>; - - commit(): Promise<void> -} diff --git a/src/sinks/IcalSink.ts b/src/sinks/IcalSink.ts deleted file mode 100644 index efb94ac..0000000 --- a/src/sinks/IcalSink.ts +++ /dev/null @@ -1,46 +0,0 @@ -import {createHash} from 'crypto'; -import {writeFile} from 'fs'; - -import * as ical from 'ical-generator'; - -import {ISink} from './ISink'; -import {IEvent} from '../core/IEvent'; - - -const sha256 = (x) => createHash('sha256').update(x, 'utf8').digest('hex'); - - -// ref. https://www.npmjs.com/package/ical-generator -export class IcalSink implements ISink { - private _cal = ical(); - private _targetFile: string; - - constructor(targetFile: string) { - this._targetFile = targetFile; - } - - createEvent(event: IEvent): Promise<void> { - // TODO better duplicate detection and resolve conflicts lol - const eventId = sha256(JSON.stringify(event)).substr(0, 16); - - const icalEvent = this._cal.createEvent({ - uid: eventId, - start: event.start.toISOString(), - end: event.end.toISOString(), - timestamp: new Date(), - summary: event.summary, - description: event.description, - }); - - return Promise.resolve(); - } - - commit(): Promise<void> { - return new Promise<void>((resolve, reject) => { - writeFile(this._targetFile, this._cal.toString(), err => { - if (err) return reject(err); - resolve(); - }); - }); - } -} |
