summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorschneefux <schneefux+commit@schneefux.xyz>2018-09-16 15:04:33 +0200
committerschneefux <schneefux+commit@schneefux.xyz>2018-09-16 15:04:33 +0200
commit3f90f2f626b6fec3c5564c7b0c7aa8914b8ef9a2 (patch)
tree8862076231786d13110d9650069178a86669c420
parent818ed0d5e2ff69bfb97074ea57b554bbc7bfcfe3 (diff)
downloadsplus-3f90f2f626b6fec3c5564c7b0c7aa8914b8ef9a2.tar.gz
splus-3f90f2f626b6fec3c5564c7b0c7aa8914b8ef9a2.zip
Hardwire ICS and fix disappearing lectures
-rw-r--r--README.md11
-rw-r--r--config-example.json9
-rw-r--r--package.json1
-rw-r--r--src/core/IEvent.ts7
-rw-r--r--src/index.ts48
-rw-r--r--src/sinks/GoogleCalendarSink.ts100
-rw-r--r--src/sinks/ISink.ts7
-rw-r--r--src/sinks/IcalSink.ts46
8 files changed, 30 insertions, 199 deletions
diff --git a/README.md b/README.md
index d38f5a1..b93ff78 100644
--- a/README.md
+++ b/README.md
@@ -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();
- });
- });
- }
-}