summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/GoogleCalendar.ts90
-rw-r--r--src/ICal.ts30
-rw-r--r--src/SplusParser.ts160
-rw-r--r--src/index.ts69
4 files changed, 349 insertions, 0 deletions
diff --git a/src/GoogleCalendar.ts b/src/GoogleCalendar.ts
new file mode 100644
index 0000000..2376c6b
--- /dev/null
+++ b/src/GoogleCalendar.ts
@@ -0,0 +1,90 @@
+import * as fs from 'fs';
+import * as readline from 'readline';
+import {google} from "googleapis";
+
+const OAuth2Client = google.auth.OAuth2;
+const scopes = ['https://www.googleapis.com/auth/calendar'];
+
+const credentialsPath = 'client_secret.json';
+const tokenPath = 'credentials.json';
+
+export 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);
+ }
+ });
+ });
+});
+
+export const calendarPromise = clientPromise.then(auth => google.calendar({version: 'v3', auth}));
+
+
+export async function listEvents() {
+ const calendar = await calendarPromise;
+ calendar.events.list({
+ calendarId: 'primary',
+ timeMin: (new Date()).toISOString(),
+ maxResults: 10,
+ singleEvents: true,
+ orderBy: 'startTime',
+ }, (err, data) => {
+ if (err) return console.log('The API returned an error: ' + err);
+ const events = data.data.items;
+ if (events.length) {
+ console.log('Upcoming 10 events:');
+ events.map((event, i) => {
+ const start = event.start.dateTime || event.start.date;
+ console.log(`${start} - ${event.summary}`);
+ });
+ } else {
+ console.log('No upcoming events found.');
+ }
+ });
+}
+
+export function createEvent(event, calendarId = 'primary') {
+ return calendarPromise.then(calendar => {
+ return new Promise((resolve, reject) => {
+ calendar.events.insert({
+ calendarId: calendarId,
+ resource: event,
+ }, function (err, event) {
+ if (err) return reject(err);
+ resolve(event);
+ });
+ })
+ });
+}
diff --git a/src/ICal.ts b/src/ICal.ts
new file mode 100644
index 0000000..09bc9bf
--- /dev/null
+++ b/src/ICal.ts
@@ -0,0 +1,30 @@
+// ref. https://www.npmjs.com/package/ical-generator
+import * as ical from 'ical-generator';
+import * as crypto from 'crypto';
+import * as fs from 'fs';
+
+const TARGET_FILE = 'etc/informatik1.ics';
+const DOMAIN = 'schneefux.github.io'; // TODO config management
+
+const sha256 = (x) => crypto.createHash('sha256').update(x, 'utf8').digest('hex');
+
+const cal = ical();
+
+// TODO declare an interface
+export function createEvent(event) {
+ // TODO better duplicate detection and resolve conflicts lol
+ const eventId = sha256(JSON.stringify(event)).substr(0, 16);
+
+ const icalEvent = cal.createEvent({
+ uid: eventId,
+ start: event.start.dateTime,
+ end: event.end.dateTime,
+ timestamp: new Date(),
+ summary: event.summary,
+ description: event.description,
+ });
+}
+
+export function commit() {
+ fs.writeFile(TARGET_FILE, cal.toString(), (err) => {});
+}
diff --git a/src/SplusParser.ts b/src/SplusParser.ts
new file mode 100644
index 0000000..be693fd
--- /dev/null
+++ b/src/SplusParser.ts
@@ -0,0 +1,160 @@
+import {load} from 'cheerio';
+
+interface IBlock {
+ title: string;
+ durationHours: number;
+ durationSlots: number;
+ info: string;
+ room: string;
+ lecturer: string
+}
+
+export interface ILecture {
+ title: string;
+ day: number;
+ begin: number;
+ end: number;
+ info: string;
+ room: string,
+ lecturer: string;
+}
+
+export class SplusParser {
+ private readonly startHour = 7;
+
+ private readonly $: CheerioStatic;
+ private readonly $table: Cheerio;
+ private readonly $rows: Cheerio;
+
+ private colWidths: number[] = [];
+ private colBlocked: number[] = [];
+
+ private blocks: IBlock[][][] = [];
+
+ constructor(data: string) {
+ this.$ = load(data);
+
+ this.$table = this.$(this.$('table.grid-border-args')[0]);
+ this.$rows = this.$table.find('> tbody > tr');
+
+ this.parseColWidths();
+ this.parseTable();
+ }
+
+ getLectures(filter?: (lecture: ILecture) => boolean): ILecture[] {
+ let lectures: ILecture[] = [];
+
+ for (let i = 0; i < this.blocks.length; i++) {
+ const begin = this.startHour + i * 0.25;
+
+ const slot = this.blocks[i];
+ for (let j = 0; j < slot.length; j++) {
+ const day = slot[j];
+ for (let k = 0; k < day.length; k++) {
+ const block = day[k];
+
+ const lecture = {
+ title: block.title,
+ day: j,
+ begin: begin,
+ end: begin + block.durationHours,
+ info: block.info,
+ room: block.room,
+ lecturer: block.lecturer
+ };
+
+ if(!filter || filter(lecture)) {
+ lectures.push(lecture);
+ }
+ }
+ }
+ }
+
+ return lectures;
+ }
+
+ private getDay(col: number) {
+ return this.colWidths.findIndex(width => (col -= width) < 0);
+ }
+
+ private parseColWidths() {
+ const $cols = this.$(this.$rows[0]).find('td:not(:first-child)');
+
+ for (let i = 0; i < $cols.length; i++) {
+ const $col = this.$($cols[i]);
+ this.colWidths[i] = parseInt($col.attr('colspan'));
+ }
+
+ let index = 0;
+ for (let i = 0; i < this.colWidths.length; i++) {
+ for (let j = 0; j < this.colWidths[i]; i++) {
+ this.colBlocked[index++] = 0;
+ }
+ }
+ }
+
+ private parseTable() {
+ for (let i = 0; i < this.$rows.length - 1; i++) {
+ // i + 1 to skip the columns row
+ const $row = this.$(this.$rows[i + 1]);
+
+ this.colBlocked = this.colBlocked.map(v => v > 0 ? v - 1 : 0);
+
+ this.blocks.push(this.parseRow($row));
+ }
+ }
+
+ private parseRow($row: Cheerio) {
+ const $cols = $row.find('> td:not(:first-child)');
+ const blocks: IBlock[][] = [];
+
+ let offset = 0;
+ for (let i = 0; i < $cols.length; i++) {
+ while (this.colBlocked[i + offset] > 0) {
+ const day = this.getDay(i + offset);
+ if (blocks[day] === undefined) {
+ blocks[day] = [];
+ }
+
+ offset++;
+ }
+
+ const day = this.getDay(i + offset);
+ if (blocks[day] === undefined) {
+ blocks[day] = [];
+ }
+
+ const $c = this.$($cols[i]);
+ if ($c.hasClass('object-cell-border')) {
+ const block = this.parseBlock($c);
+ blocks[day].push(block);
+
+ this.colBlocked[i + offset] = block.durationSlots;
+ }
+ }
+
+ return blocks;
+ }
+
+ private parseBlock($block: Cheerio): IBlock {
+ const $tblTitle = $block.find('table:nth-child(1)');
+ const $tblInfo = $block.find('table:nth-child(2)');
+ const $tblLocation = $block.find('table:nth-child(3)');
+
+ const $tdRoom = $tblLocation.find('td:nth-child(1)');
+ const $tdLecturer = $tblLocation.find('td:nth-child(2)');
+
+ // Remove the Modulhandbuch "mhb" link
+ $tblTitle.find('br + strong').remove();
+
+ const slots = parseInt($block.attr('rowspan'));
+ return {
+ title: $tblTitle.text().trim(),
+ durationHours: slots / 4,
+ durationSlots: slots,
+ info: $tblInfo.text().trim(),
+ room: $tdRoom.text().trim(),
+ lecturer: $tdLecturer.text().trim()
+ };
+ }
+}
diff --git a/src/index.ts b/src/index.ts
new file mode 100644
index 0000000..4791fb2
--- /dev/null
+++ b/src/index.ts
@@ -0,0 +1,69 @@
+import {readFile} from 'fs';
+import {SplusParser} from './SplusParser';
+let createEvent = (arg) => {};
+let commit = () => {};
+if (process.argv.includes('ics')) {
+ createEvent = require('./ICal').createEvent;
+ commit = require('./ICal').commit;
+} else {
+ createEvent = require('./GoogleCalendar').createEvent;
+}
+
+
+const splusUrl = 'http://splus.ostfalia.de/semesterplan123.php?id=1362F014835FFFD0F67159E302EC1A3C&identifier=%23SPLUS7A3292';
+
+/*
+get(splusUrl, (res) => {
+ let data = '';
+ res.on('data', chunk => data += chunk);
+ res.on('end', () => TODO);
+});
+*/
+
+const baseDate = new Date(2018, 3, 23);
+
+readFile('etc/sample.htm', (err, data) => {
+ const lectures = new SplusParser(data.toString()).getLectures(lecture => {
+ // Filter some lectures out
+ if (lecture.title === '') return false;
+ if (lecture.title.indexOf('Mathe-Cafe') !== -1) return false;
+ if (lecture.title.indexOf('Mathe-Repetitorium') !== -1) return false;
+ if (lecture.title.indexOf('Informatik-Lounge') !== -1) return false;
+ if (lecture.title.indexOf('Reservierung') !== -1) return false;
+ return lecture.title.indexOf('Kompetenzen') === -1;
+ });
+
+ lectures.forEach((lec, index) => {
+ let beginDate = new Date(baseDate);
+
+ beginDate.setDate(beginDate.getDate() + lec.day);
+ beginDate.setMinutes(beginDate.getMinutes() + lec.begin * 60);
+
+ let endDate = new Date(baseDate);
+ endDate.setDate(endDate.getDate() + lec.day);
+ endDate.setMinutes(endDate.getMinutes() + lec.end * 60);
+
+ console.log(lec.title, beginDate.toISOString(), endDate.toISOString());
+
+ createEvent({
+ summary: lec.title,
+ description: lec.lecturer + (lec.info !== '' ? ' - ' : '') + lec.info,
+ location: lec.room,
+ start: {
+ dateTime: beginDate.toISOString()
+ },
+ end: {
+ dateTime: endDate.toISOString()
+ },
+ reminders: {
+ useDefault: false
+ }
+ });
+
+ // TODO
+ if (index == lectures.length - 1) {
+ commit();
+ }
+ })
+});
+