dsbiserv
Version:
A library to query the dsb plan from iServ
517 lines (483 loc) • 21.1 kB
JavaScript
/**
* Created by nilsbergmann on 16.01.17.
*/
const request = require('request');
const fs = require('fs-extra');
const cheerio = require('cheerio');
const _ = require('lodash');
const async = require('async');
const tabletojson = require('tabletojson');
const rangeParser = require('parse-numeric-range');
const iconv = require('iconv-lite');
const EventEmitter = require("events").EventEmitter;
const WatchJS = require('watchjs');
class DSBLibrary {
/**
*
* @param {string} iServHost
* @param {string} iServUsername
* @param {string} iServPassword
* @param {string} CookieFile - Path to file with cookies (iServ login cookie cache)
* @param {boolean} [debugOutput]y
*/
constructor(iServHost, iServUsername, iServPassword, CookieFile, debugOutput, planURL) {
this.host = iServHost;
this.username = iServUsername;
this.password = iServPassword;
this.debug = debugOutput ? debugOutput : false;
this.log = function (message) {
if (this.debug) {
console.log(message);
}
};
this.cookies = null;
this.cookie_cache = CookieFile ? CookieFile : null;
try {
this.cookies = fs.readJsonSync(CookieFile);
} catch (e) {
this.cookies = null;
}
this.planURL = planURL ? planURL : "iserv/plan/show/raw/DSB%20Schueler";
this.Events = new EventEmitter();
this.Progress = 0;
WatchJS.watch(this, 'Progress', () => {
this.Events.emit('progress', this.Max, this.Progress);
});
}
/**
* Download the dsb plans and get them parsed.
* @param {function} DSBCallback
*/
getParsed(DSBCallback) {
this.Progress = 0;
this.Max = 11;
this.getDSBPlans((error, Plans) => {
if (error) {
DSBCallback(error);
} else {
let ResultPlans = [];
async.each(Plans, (Plan, EachCallback) => {
this.parsePlan(Plan.html, (error, BPlan) => {
if (error) return EachCallback(error);
ResultPlans.push({
date: Plan.date,
plan: BPlan,
lastUpdate: DSBLibrary._getStatus(Plan.html),
html: Plan.html
});
EachCallback();
});
}, (error) => {
this.Progress += this.Max - this.Progress;
DSBCallback(error, ResultPlans);
});
}
});
}
getDSBPlans(DSBCallback) {
async.waterfall([
(WaterfallCallback) => {
if (this.cookies) {
this._checkCookies((error) => {
if (error) {
this.log(`[DEBUG] ${error}`);
this._login(WaterfallCallback);
} else {
WaterfallCallback();
}
});
} else {
this._login(WaterfallCallback);
}
},
(WaterfallCallback) => this._DoRequest(`https://${this.host}/${this.planURL}`, null, false, WaterfallCallback),
(HTML, WaterfallCallback) => WaterfallCallback(null, DSBLibrary._FastIFrameParse(HTML, 'iframe[name="DSBHPLehrer"]')),
(URL, WaterfallCallback) => this._DoRequest(URL, null, true, WaterfallCallback),
(HTML, URLReferer, WaterfallCallback) => WaterfallCallback(null, DSBLibrary._FastIFrameParse(HTML, 'iframe'), URLReferer),
(URL, URLReferer, WaterfallCallback) => this._DoRequest(URL, URLReferer, false, WaterfallCallback),
(HTML, WaterfallCallback) => {
/**
* Now lets get both plans: Vertretungsplan-Modul, Vertretungsplan-Modul 2
*/
let Plans = [];
async.each(DSBLibrary._FastIFrameParse(HTML, ['iframe[name="Vertretungsplan-Modul"]', 'iframe[name="Vertretungsplan-Modul 2"]']), (IFrame, EachCallback) => {
const URL = IFrame.src;
if (URL) {
this._DoRequest(URL, null, true, (error, HTML, Referer) => {
if (error) {
EachCallback(error);
} else {
const PlanURL = DSBLibrary._GetThisShittyJSLocationHref(HTML);
this._DoRequest(PlanURL, Referer, false, (error, HTML) => {
this._DoRequest(DSBLibrary._FastIFrameParse(HTML, 'iframe'), PlanURL, false, (error, HTML) => {
if (error) {
EachCallback(error);
} else {
const $ = cheerio.load(HTML);
const MonTitle = $(".mon_title");
const regexForDate = /^(?:(?:31(\/|-|\.)(?:0?[13578]|1[02]))\1|(?:(?:29|30)(\/|-|\.)(?:0?[1,3-9]|1[0-2])\2))(?:(?:1[6-9]|[2-9]\d)?\d{2})$|^(?:29(\/|-|\.)0?2\3(?:(?:(?:1[6-9]|[2-9]\d)?(?:0[48]|[2468][048]|[13579][26])|(?:(?:16|[2468][048]|[3579][26])00))))$|^(?:0?[1-9]|1\d|2[0-8])(\/|-|\.)(?:(?:0?[1-9])|(?:1[0-2]))\4(?:(?:1[6-9]|[2-9]\d)?\d{2})/;
const date = regexForDate.exec(MonTitle.contents()[0].data.split(' ')[0])[0];
Plans.push({
date: date,
html: HTML
});
EachCallback();
}
});
});
}
});
} else {
EachCallback();
}
}, (error) => {
WaterfallCallback(error, Plans);
});
}
], (error, Plans) => {
if (error) this.log(error);
DSBCallback(error, Plans);
});
}
/**
* Login into iServ
* @param LoginCallback
* @private
*/
_login(LoginCallback) {
this.log(`[DEBUG] Start login process.`);
/**
* Send login request
*/
request.post(`https://${this.host}/iserv/login_check`, {
form: {
_username: this.username,
_password: this.password
}
}, (error, response) => {
if (!error && response.statusCode == 302) {
if (response.headers["set-cookie"]) {
let cookies = {};
/**
* Parse the 'set-cookie' to a use able format.
*/
for (let u in response.headers["set-cookie"]) {
if (response.headers["set-cookie"].hasOwnProperty(u)) {
const current = response.headers["set-cookie"][u];
const m = /^[^;]+/.exec(current);
if (m) {
const CookieSplit = m[0].split("=");
if (CookieSplit[0] && CookieSplit[1]) {
cookies[CookieSplit[0]] = CookieSplit[1];
}
}
}
}
this.cookies = cookies;
this._makeCookieHeaderString();
this.log(`[DEBUG] Login successfully with user ${this.username}!`);
if (this.cookie_cache) {
this.Progress += 1;
fs.writeFile(this.cookie_cache, JSON.stringify(this.cookies), LoginCallback);
} else {
this.Progress += 1;
LoginCallback();
}
} else {
/**
* With the header 'set-cookie' sets iServ the login sessions. If there is no header 'set-cookie' this error appears.
*/
LoginCallback(`No 'set-cookie' found. Login not successfully ?`);
this.Progress += 1;
}
} else {
/**
* The response status code needs to be 302 if not this error appears.
*/
LoginCallback(`Something went wrong. Response code: ${response.statusCode} | Error: ${error}`);
this.Progress += 1;
}
});
}
/**
* Checks if the cookies are still valid
* @param CookieCallback
* @private
*/
_checkCookies(CookieCallback) {
this.log(`[DEBUG] Check if your cookies are still valid.`);
if (this.cookies) {
/**
* Header cookie string from cookies.
* @type {string}
*/
this._makeCookieHeaderString();
request(`https://${this.host}/iserv/user/api/notifications`, {
headers: {
Cookie: this.cookieHeader
},
followRedirect: false
}, (error, response, body) => {
if (!error && response.statusCode == 200) {
this.Progress += 1;
CookieCallback();
} else {
this.Progress += 1;
CookieCallback(`It seems that your cookies are no longer valid! Response status code ${response.statusCode} | Error ${error}`);
}
});
} else {
this.Progress += 1;
CookieCallback(`Where are my cookies !!?? This should never happen!`);
}
}
/**
* Make cookie header string
* @private
*/
_makeCookieHeaderString() {
let cookieString = "";
for (let CookieIndex in this.cookies) {
if (this.cookies.hasOwnProperty(CookieIndex)) {
cookieString = cookieString + `${CookieIndex}=${this.cookies[CookieIndex]}; `;
}
}
this.cookieHeader = cookieString;
}
/**
* Fast parse the html and get the src of a iFrame with given selector
* @param HTML
* @param {Array|string} Selector
* @returns {Array|jQuery}
* @private
*/
static _FastIFrameParse(HTML, Selectors) {
const $ = cheerio.load(HTML);
if (_.isArray(Selectors)) {
let returnArray = [];
for (let index in Selectors) {
if (Selectors.hasOwnProperty(index)) {
returnArray.push({
selector: Selectors[index],
src: $(Selectors[index]).attr('src')
});
}
}
return returnArray;
} else {
return $(Selectors).attr('src');
}
}
/**
* Fast do a request and handel errors
* @param URL
* @param Referer - The on going url to fake a iFrame request
* @param WithURL
* @param RequestCallback
* @private
*/
_DoRequest(URL, Referer, WithURL, RequestCallback) {
let options = {
headers: {
Cookie: this.cookieHeader,
"Accept-Encoding": "UTF-8",
"Content-Type": "text/plain; charset=utf-8;",
},
encoding: null
};
if (Referer) {
options.headers.Referer = Referer;
}
this.log(`[DEBUG] Do request to ${URL} with this options: ${JSON.stringify(options)}`);
request(URL, options, (error, response, body) => {
if (!error && response.statusCode == 200) {
this.log(`[DEBUG] Successfully!`);
if (WithURL) {
this.Progress += 1;
RequestCallback(null, iconv.decode(Buffer.from(body), 'cp1252'), URL);
} else {
this.Progress += 1;
RequestCallback(null, iconv.decode(Buffer.from(body), 'cp1252'));
}
} else {
this.Progress += 1;
RequestCallback(`Hmm. Something happened. Maybe you know what to do with this status code ${response.statusCode} and this error ${error}`);
}
});
}
/**
* Parse the html and fast get this shitty location.href url
* @param HTML
* @returns {XML|*|void|String|string}
* @private
*/
static _GetThisShittyJSLocationHref(HTML) {
const superRegex = /location.href=".+(?=";)/;
const m = superRegex.exec(HTML);
return m[0].replace('location.href="', '');
}
/**
* Parse the DSB Plan in a usable format
* @param {String} HTMLPlan
* @param {function} ParseCallback
*/
parsePlan(HTMLPlan, ParseCallback) {
const $ = cheerio.load(HTMLPlan);
const PlanTableHTML = $('table[class="mon_list"]').parent().html();
const TableJson = tabletojson.convert(PlanTableHTML);
if (TableJson.length == 1) {
this.Progress += 1;
ParseCallback(null, DSBLibrary._reformatPlanJSON(TableJson[0]));
} else {
this.Progress += 1;
ParseCallback(`Something went wrong. Maybe they changed the format ?`);
}
}
/**
* Reformat the plan json
* @param {JSON} JSON
* @returns {JSON}
* @private
*/
static _reformatPlanJSON(JSON) {
const Plan = JSON;
let needToRemove = [];
for (let PlanIndex in Plan) {
if (Plan.hasOwnProperty(PlanIndex)) {
if (needToRemove.indexOf(PlanIndex) != -1) continue;
let LinesToAdd = [];
for (let TestForMultiLinesIndex in Plan) {
if (!Plan.hasOwnProperty(TestForMultiLinesIndex)) continue;
if (parseInt(TestForMultiLinesIndex) <= parseInt(PlanIndex)) continue;
if (Plan[TestForMultiLinesIndex]) {
if (!Plan[TestForMultiLinesIndex]["Std."] && !Plan[TestForMultiLinesIndex]["Fach"] && !Plan[TestForMultiLinesIndex]["Lehrer"] && !Plan[TestForMultiLinesIndex]["statt"] && !Plan[TestForMultiLinesIndex]["Raum"] && parseInt(TestForMultiLinesIndex) > parseInt(PlanIndex)) {
LinesToAdd.push(TestForMultiLinesIndex);
} else {
if (parseInt(TestForMultiLinesIndex) > parseInt(PlanIndex)) {
break;
}
}
} else {
break;
}
}
for (let LinesToAddIndex in LinesToAdd) {
if (LinesToAdd.hasOwnProperty(LinesToAddIndex)) {
Plan[PlanIndex]['Klasse(n)'] = Plan[PlanIndex]['Klasse(n)'] + Plan[LinesToAdd[LinesToAddIndex]]['Klasse(n)'];
Plan[PlanIndex]['Bemerkungen'] = Plan[PlanIndex]['Bemerkungen'] + Plan[LinesToAdd[LinesToAddIndex]]['Bemerkungen'];
}
}
needToRemove = needToRemove.concat(LinesToAdd);
if (Plan[PlanIndex].Entfall != null && typeof Plan[PlanIndex].Entfall == 'string') Plan[PlanIndex].Entfall = Plan[PlanIndex].Entfall.toLowerCase() == "x";
for (let key in Plan[PlanIndex]) {
if (Plan[PlanIndex].hasOwnProperty(key)) {
if (typeof Plan[PlanIndex][key] == 'string') {
Plan[PlanIndex][key] = Plan[PlanIndex][key].trim();
if (Plan[PlanIndex][key] == "") Plan[PlanIndex][key] = null;
}
}
}
if (Plan[PlanIndex]['Std.']) {
Plan[PlanIndex]['Std.'] = Plan[PlanIndex]['Std.'].replaceAll(/\s/, '');
if (Plan[PlanIndex]['Std.'].includes('-')) {
Plan[PlanIndex].Stunden = rangeParser.parse(Plan[PlanIndex]['Std.']);
} else {
Plan[PlanIndex].Stunden = [Plan[PlanIndex]['Std.']];
}
}
if (Plan[PlanIndex].Raum == "---") {
Plan[PlanIndex].Raum = null;
}
// 5678910111213ABBLCDMNMZAG -> JSON
if (Plan[PlanIndex]['Klasse(n)']) {
let ClassParts = [];
const ClassesPartsRegex = /\d+\D+/g;
let m;
while ((m = ClassesPartsRegex.exec(Plan[PlanIndex]['Klasse(n)'])) !== null) {
if (m.index === ClassesPartsRegex.lastIndex) {
ClassesPartsRegex.lastIndex++;
}
ClassParts.push(m[0]);
}
Plan[PlanIndex].KlassenStufen = [];
Plan[PlanIndex].KlassenBuchstaben = [];
Plan[PlanIndex].Klassen = [];
for (let ClassPartsIndex in ClassParts) {
if (!ClassParts.hasOwnProperty(ClassPartsIndex)) continue;
const CompleteClass = ClassParts[ClassPartsIndex];
const Years = CompleteClass.replaceAll(/\D/, '');
const Classes = CompleteClass.replaceAll(/\d/, '');
let localKlassenStufen = [];
let localKlassenBuchstaben = [];
for (let i = 5; i <= 13; i++) {
if (Years.match(i)) {
Plan[PlanIndex].KlassenStufen.push(i);
localKlassenStufen.push(i);
}
}
const ClassCharList = [
/A(?!G)/,
/B(?!L)/,
"C",
"D",
"BL",
"MN",
"MZ",
"AG"
];
for (let i = 0; i < ClassCharList.length; i++) {
const match = Classes.match(ClassCharList[i]);
if (match) localKlassenBuchstaben.push(match[0]);
if (match && Plan[PlanIndex].KlassenBuchstaben.indexOf(match[0]) == -1) Plan[PlanIndex].KlassenBuchstaben.push(match[0]);
}
let localKlassen = [];
for (let Index in localKlassenStufen) {
if (localKlassenStufen.hasOwnProperty(Index)) {
const Stufe = localKlassenStufen[Index];
for (let BIndex in localKlassenBuchstaben) {
if (localKlassenBuchstaben.hasOwnProperty(BIndex)) {
const Buchstabe = localKlassenBuchstaben[BIndex];
localKlassen.push(`${Stufe}${Buchstabe}`);
}
}
}
}
Plan[PlanIndex]['Klassen'] = Plan[PlanIndex]['Klassen'].concat(localKlassen);
}
Plan[PlanIndex].GanzerJahrgang = Plan[PlanIndex].KlassenBuchstaben.length == 0 && Plan[PlanIndex].Klassen.length == 0;
}
}
}
//console.log(needToRemove);
Plan.multisplice.apply(Plan, needToRemove);
return Plan;
}
/**
* Get the status
* @param HTML
* @private
*/
static _getStatus(HTML) {
const StatusRegex = /Stand:.*<\/p>/;
let m = StatusRegex.exec(HTML)
return m[0].replaceAll('Stand:', '').replaceAll('</p>', '').trim();
}
}
String.prototype.replaceAll = function (search, replacement) {
let target = this;
return target.replace(new RegExp(search, 'g'), replacement);
};
/**
* @author http://upshots.org/actionscript/javascript-splice-array-on-multiple-indices-multisplice
*/
Array.prototype.multisplice = function () {
let args = Array.apply(null, arguments);
args.sort(function (a, b) {
return a - b;
});
for (let i = 0; i < args.length; i++) {
let index = args[i] - i;
this.splice(index, 1);
}
};
module.exports = DSBLibrary;