aboutsummaryrefslogtreecommitdiff
path: root/src/server
diff options
context:
space:
mode:
Diffstat (limited to 'src/server')
-rw-r--r--src/server/app.js64
-rwxr-xr-xsrc/server/bin/www60
-rw-r--r--src/server/lib/getMeetingpointData.js83
-rw-r--r--src/server/lib/getURLOfUser.js8
-rw-r--r--src/server/lib/getUserIndex.js85
-rw-r--r--src/server/routes/getSchedule.js76
-rw-r--r--src/server/routes/index.js31
-rw-r--r--src/server/routes/manifest.js19
-rw-r--r--src/server/routes/opensearch.js12
-rw-r--r--src/server/routes/slack.js60
10 files changed, 498 insertions, 0 deletions
diff --git a/src/server/app.js b/src/server/app.js
new file mode 100644
index 0000000..3477ed5
--- /dev/null
+++ b/src/server/app.js
@@ -0,0 +1,64 @@
+const express = require('express')
+const path = require('path')
+const logger = require('morgan')
+const cookieParser = require('cookie-parser')
+const bodyParser = require('body-parser')
+const compression = require('compression')
+
+const routes = require('./routes/index')
+const getSchedule = require('./routes/getSchedule')
+const manifest = require('./routes/manifest')
+const slack = require('./routes/slack')
+
+const app = express()
+
+app.use(compression())
+
+// view engine setup
+app.set('views', path.join(__dirname, '../client/views'))
+app.set('view engine', 'jade')
+
+app.use(logger('dev'))
+app.use(bodyParser.json())
+app.use(bodyParser.urlencoded({ extended: false }))
+app.use(cookieParser())
+
+app.use('/manifest.webmanifest', manifest)
+app.use(express.static(path.join(__dirname, '../client/static')))
+
+app.use('/', routes)
+app.use('/get', getSchedule)
+app.use('/slack', slack)
+
+// catch 404 and forward to error handler
+app.use(function (req, res, next) {
+ const err = new Error('Not Found')
+ err.status = 404
+ next(err)
+})
+
+// error handlers
+
+// development error handler
+// will print stacktrace
+if (app.get('env') === 'development') {
+ app.use(function (err, req, res, next) {
+ res.status(err.status || 500)
+ res.render('error', {
+ message: err.message,
+ error: err
+ })
+ })
+}
+
+// production error handler
+// no stacktraces leaked to user
+app.use(function (err, req, res, next) {
+ res.status(err.status || 500)
+ res.render('error', {
+ message: err.message,
+ error: {}
+ })
+})
+
+module.exports = app
diff --git a/src/server/bin/www b/src/server/bin/www
new file mode 100755
index 0000000..545db41
--- /dev/null
+++ b/src/server/bin/www
@@ -0,0 +1,60 @@
+#!/usr/bin/env node
+
+const app = require('../app')
+const http = require('http')
+
+const port = normalizePort(process.env.PORT || '3000')
+const server = http.createServer(app)
+
+server.listen(port)
+server.on('error', error => onError(error, port))
+server.on('listening', _ => onListening(server))
+
+function normalizePort (val) {
+ const port = parseInt(val, 10)
+
+ if (isNaN(port)) {
+ // named pipe
+ return val
+ }
+
+ if (port >= 0) {
+ // port number
+ return port
+ }
+
+ return false
+}
+
+function onError (error, port) {
+ if (error.syscall !== 'listen') {
+ throw error
+ }
+
+ const bind = typeof port === 'string'
+ ? 'Pipe ' + port
+ : 'Port ' + port
+
+ // handle specific listen errors with friendly messages
+ switch (error.code) {
+ case 'EACCES':
+ console.error(bind + ' requires elevated privileges')
+ process.exit(1)
+ break
+ case 'EADDRINUSE':
+ console.error(bind + ' is already in use')
+ process.exit(1)
+ break
+ default:
+ throw error
+ }
+}
+
+function onListening (server) {
+ const addr = server.address()
+ if (typeof addr === 'string') {
+ console.log(`Listening on pipe ${addr}`)
+ } else {
+ console.log(`Listening on http://localhost:${addr.port}/`)
+ }
+}
diff --git a/src/server/lib/getMeetingpointData.js b/src/server/lib/getMeetingpointData.js
new file mode 100644
index 0000000..94cf36c
--- /dev/null
+++ b/src/server/lib/getMeetingpointData.js
@@ -0,0 +1,83 @@
+'use strict'
+
+const Promise = require('bluebird')
+const cheerio = require('cheerio')
+const _ = require('lodash')
+const request = Promise.promisify(require('request'))
+
+let meetingpointData
+let lastUpdate
+
+function getUsers (page) {
+ const script = page('script').eq(1).text()
+
+ const regexs = [/var classes = \[(.+)\];/, /var teachers = \[(.+)\];/, /var rooms = \[(.+)\];/, /var students = \[(.+)\];/]
+ const items = regexs.map(function (regex) {
+ return script.match(regex)[1].split(',').map(function (item) {
+ return item.replace(/"/g, '')
+ })
+ })
+
+ return []
+ .concat(items[0].map(function (item, index) {
+ return {
+ type: 'c',
+ value: item,
+ index: index
+ }
+ }))
+ .concat(items[1].map(function (item, index) {
+ return {
+ type: 't',
+ value: item,
+ index: index
+ }
+ }))
+ .concat(items[2].map(function (item, index) {
+ return {
+ type: 'r',
+ value: item,
+ index: index
+ }
+ }))
+ .concat(items[3].map(function (item, index) {
+ return {
+ type: 's',
+ value: item,
+ index: index
+ }
+ }))
+}
+
+function getValidWeekNumbers(page) {
+ const weekSelector = page('select[name="week"]');
+ const weekNumbers = _.map(weekSelector.children(), option => parseInt(option.attribs.value))
+
+ return weekNumbers;
+}
+
+function requestData() {
+ lastUpdate = new Date()
+
+ return request(`http://www.meetingpointmco.nl/Roosters-AL/doc/dagroosters/frames/navbar.htm`, { timeout: 5000 }).then((response) => {
+ const page = cheerio.load(response.body)
+ const users = getUsers(page)
+ const validWeekNumbers = getValidWeekNumbers(page)
+
+ meetingpointData = { users, validWeekNumbers }
+
+ return meetingpointData
+ })
+}
+
+function getMeetingpointData () {
+ if (lastUpdate == null || new Date() - lastUpdate > 10 * 60 * 1000) { // 10 minutes
+ return requestData()
+ } else if (!meetingpointData) {
+ return Promise.reject()
+ } else {
+ return Promise.resolve(meetingpointData)
+ }
+}
+
+module.exports = getMeetingpointData
diff --git a/src/server/lib/getURLOfUser.js b/src/server/lib/getURLOfUser.js
new file mode 100644
index 0000000..2de48e6
--- /dev/null
+++ b/src/server/lib/getURLOfUser.js
@@ -0,0 +1,8 @@
+const leftPad = require('left-pad') // I imported this just to piss you off ;)
+
+function getURLOfUser (type, index, week) {
+ return `http://www.meetingpointmco.nl/Roosters-AL/doc/dagroosters/` +
+ `${leftPad(week, 2, '0')}/${type}/${type}${leftPad(index + 1, 5, '0')}.htm`
+}
+
+module.exports = getURLOfUser
diff --git a/src/server/lib/getUserIndex.js b/src/server/lib/getUserIndex.js
new file mode 100644
index 0000000..db7daa8
--- /dev/null
+++ b/src/server/lib/getUserIndex.js
@@ -0,0 +1,85 @@
+'use strict'
+
+const Promise = require('bluebird')
+const cheerio = require('cheerio')
+const request = Promise.promisify(require('request'))
+
+let userIndex
+let lastUpdate
+
+function updateUserIndex () {
+ return new Promise(function (resolve, reject) {
+ process.stdout.write('Updating user index... ')
+ request(`http://www.meetingpointmco.nl/Roosters-AL/doc/dagroosters/frames/navbar.htm`)
+ .then(function (page) {
+ lastUpdate = new Date()
+ page = page.body
+
+ const $ = cheerio.load(page)
+ const $script = $('script').eq(1)
+ const scriptText = $script.text()
+
+ const regexs = [/var classes = \[(.+)];/, /var teachers = \[(.+)];/, /var rooms = \[(.+)];/, /var students = \[(.+)];/]
+ const items = regexs.map(function (regex) {
+ return scriptText.match(regex)[1].split(',').map(function (item) {
+ return item.replace(/"/g, '')
+ })
+ })
+
+ userIndex = ([]
+ .concat(items[0].map(function (item, index) {
+ return {
+ type: 'c',
+ value: item,
+ index: index
+ }
+ }))
+ .concat(items[1].map(function (item, index) {
+ return {
+ type: 't',
+ value: item,
+ index: index
+ }
+ }))
+ .concat(items[2].map(function (item, index) {
+ return {
+ type: 'r',
+ value: item,
+ index: index
+ }
+ }))
+ .concat(items[3].map(function (item, index) {
+ return {
+ type: 's',
+ value: item,
+ index: index
+ }
+ })))
+
+ process.stdout.write('done.\n')
+
+ resolve(userIndex)
+ })
+ .catch(error => {
+ process.stdout.write('failed.\n')
+ reject(error)
+ })
+ })
+}
+
+function getUserIndex () {
+ return new Promise((resolve, reject) => {
+ if (lastUpdate == null) {
+ updateUserIndex().then(resolve, reject)
+ } else if (new Date() - lastUpdate > 10 * 60 * 1000) { // 10 minutes
+ updateUserIndex().then(resolve, function () {
+ console.warn('Unable to update userIndex, using cached.')
+ resolve(userIndex)
+ })
+ } else {
+ resolve(userIndex)
+ }
+ })
+}
+
+module.exports = getUserIndex
diff --git a/src/server/routes/getSchedule.js b/src/server/routes/getSchedule.js
new file mode 100644
index 0000000..7850918
--- /dev/null
+++ b/src/server/routes/getSchedule.js
@@ -0,0 +1,76 @@
+const express = require('express')
+const router = express.Router()
+const request = require('request')
+const iconv = require('iconv-lite')
+const webshot = require('webshot')
+
+const getUserIndex = require('../lib/getUserIndex')
+const getURLOfUser = require('../lib/getURLOfUser')
+
+// copied from http://www.meetingpointmco.nl/Roosters-AL/doc/dagroosters/untisscripts.js,
+// were using the same code as they do to be sure that we always get the same
+// week number.
+function getWeekNumber (target) {
+ const dayNr = (target.getDay() + 6) % 7
+ target.setDate(target.getDate() - dayNr + 3)
+ const firstThursday = target.valueOf()
+ target.setMonth(0, 1)
+ if (target.getDay() !== 4) {
+ target.setMonth(0, 1 + ((4 - target.getDay()) + 7) % 7)
+ }
+
+ return 1 + Math.ceil((firstThursday - target) / 604800000)
+}
+
+router.get('/:type/:value.png', function (req, res, next) {
+ port = process.env.PORT || 3000;
+ const { type, value } = req.params
+ const stream = webshot(
+ `http://localhost:${port}/get/${type}/${value}`,
+ { customCSS: "body { background-color: white; }" }
+ )
+ stream.pipe(res)
+})
+
+router.get('/:type/:value.jpg', function (req, res, next) {
+ port = process.env.PORT || 3000;
+ const { type, value } = req.params
+ const stream = webshot(
+ `http://localhost:${port}/get/${type}/${value}`,
+ { customCSS: "body { background-color: white; }", streamType: 'jpg' }
+ )
+ stream.pipe(res)
+})
+
+router.get('/:type/:value', function (req, res, next) {
+ getUserIndex().then(users => {
+ const { type, value } = req.params
+ let { week } = req.query
+ const user =
+ users.filter(user => user.type === type && user.value === value)[0]
+
+ if (!user) {
+ next(new Error(`${type}${value} is not in the user index.`))
+ }
+
+ if (!week) {
+ week = getWeekNumber(new Date())
+ }
+
+ const { index } = user
+
+ const url = getURLOfUser(type, index, week)
+
+ request(url, { encoding: null }, function (err, data) {
+ if (err) {
+ next(err)
+ return
+ }
+
+ const utf8Body = iconv.decode(data.body, 'ISO-8859-1')
+ res.status(data.statusCode).end(utf8Body)
+ })
+ })
+})
+
+module.exports = router
diff --git a/src/server/routes/index.js b/src/server/routes/index.js
new file mode 100644
index 0000000..d2267ba
--- /dev/null
+++ b/src/server/routes/index.js
@@ -0,0 +1,31 @@
+'use strict'
+
+const express = require('express')
+const router = express.Router()
+const getMeetingpointData = require('../lib/getMeetingpointData')
+
+/* GET home page. */
+router.get(['/', '/s/*', '/t/*', '/r/*', '/c/*'], function (req, res, next) {
+ getMeetingpointData().then(data => {
+ const isBeta = process.env.BETA === '1'
+
+ let flags = []
+ if (isBeta) {
+ flags.push('BETA')
+ flags.push('NO_FEATURE_DETECT')
+ } else if (req.query.nfd != null) {
+ flags.push('NO_FEATURE_DETECT')
+ }
+
+ const flagsStr = `var FLAGS = ${JSON.stringify(flags)};`
+ const usersStr = `var USERS = ${JSON.stringify(data.users)};`
+ const validWeekNumbersStr = `var VALID_WEEK_NUMBERS = ${JSON.stringify(data.validWeekNumbers)}`
+
+ res.render('index', { flagsStr, usersStr, validWeekNumbersStr })
+ }).catch(function () {
+ console.error('Unable to get user info, emergency redirect!')
+ res.render('redirect')
+ })
+})
+
+module.exports = router
diff --git a/src/server/routes/manifest.js b/src/server/routes/manifest.js
new file mode 100644
index 0000000..b2ce55f
--- /dev/null
+++ b/src/server/routes/manifest.js
@@ -0,0 +1,19 @@
+'use strict'
+
+const express = require('express')
+const router = express.Router()
+const path = require('path')
+
+router.get('/', function (req, res, next) {
+ console.log('got a request')
+
+ const isBeta = process.env.BETA === '1'
+
+ if (isBeta) {
+ res.sendFile('manifest.beta.webmanifest', { root: path.join(__dirname, '../public') })
+ } else {
+ res.sendFile('manifest.webmanifest', { root: path.join(__dirname, '../public') })
+ }
+})
+
+module.exports = router
diff --git a/src/server/routes/opensearch.js b/src/server/routes/opensearch.js
new file mode 100644
index 0000000..c3e2e57
--- /dev/null
+++ b/src/server/routes/opensearch.js
@@ -0,0 +1,12 @@
+'use strict'
+
+const express = require('express')
+const router = express.Router()
+const path = require('path')
+
+router.get('/', function (req, res, next) {
+ res.setHeader('content-type', 'application/opensearchdescription+xml')
+ res.sendFile('opensearch.xml', { root: path.join(__dirname, '../public') })
+})
+
+module.exports = router
diff --git a/src/server/routes/slack.js b/src/server/routes/slack.js
new file mode 100644
index 0000000..e256f14
--- /dev/null
+++ b/src/server/routes/slack.js
@@ -0,0 +1,60 @@
+const express = require('express')
+const router = express.Router()
+
+const getUserIndex = require('../lib/getUserIndex')
+
+function generateResponse(query) {
+ return getUserIndex().then(users => {
+ const user =
+ users.filter(user => user.value === query)[0]
+
+ if (!user) {
+ return {
+ "response_type": "ephemeral",
+ "mrkdwn": true,
+ "text": `Sorry, I tried my best, but I couldn't find _${query}_`
+ }
+ }
+
+ return {
+ "response_type": "in_channel",
+ "text": `Here is the schedule of _${query}_`,
+ "mrkdwn": true,
+ "attachments": [
+ {
+ "fallback": `https://beta.rooster.hetmml.nl/${user.type}/${user.value}`,
+ "image_url": `https://beta.rooster.hetmml.nl/get/${user.type}/${user.value}.png`
+ }
+ ]
+ }
+ })
+}
+
+router.all('/', function (req, res, next) {
+ const query = req.body.text || req.query.text
+
+ if (query.indexOf('!') === 0) {
+ switch (query) {
+ case "!help":
+ res.json({
+ "response_type": "ephemeral",
+ "mrkdwn": true,
+ "text": "Here are some examples on how you can use me\n>/rooster 18561\n\n>/rooster akh\n\n>/rooster 6-5H2\n\n>/rooster 008-mk\n\nPlease note that the following does not work (yet)",
+ "attachments": [{
+ "text": "/rooster 5h2",
+ "color": "danger"
+ }]
+ })
+ default:
+ res.json({
+ "response_type": "ephemeral",
+ "text": "Unrecognized command, try !help"
+ })
+ break;
+ }
+ }
+
+ generateResponse(query).then((json) => res.json(json))
+})
+
+module.exports = router