diff options
Diffstat (limited to 'lib')
| -rw-r--r-- | lib/controllers/main_menu.js | 96 | ||||
| -rw-r--r-- | lib/controllers/recording_menu.js | 82 | ||||
| -rw-r--r-- | lib/controllers/recordings.js | 166 | ||||
| -rw-r--r-- | lib/dead_drop.js | 72 |
4 files changed, 381 insertions, 35 deletions
diff --git a/lib/controllers/main_menu.js b/lib/controllers/main_menu.js new file mode 100644 index 0000000..461cff8 --- /dev/null +++ b/lib/controllers/main_menu.js @@ -0,0 +1,96 @@ +'use strict'; + +const Twilio = require('twilio'); + +const internals = {}; + +internals.kMenuTimeout = 10; // timeout in seconds +internals.kMenuDigits = 1; // number of digits in the menu +internals.kContentType = 'application/xml'; // The content type used to respond +internals.kMenuLanguage = 'es-mx'; // the language to use +internals.kTimeoutMessage = 'Oh... está bien. Adios!'; +internals.kMainMenuRoute = '/menus/main'; +internals.kRecordingMenuRoute = '/menus/recording'; +internals.kRandomMessageRoute = '/recordings/0'; +internals.kLeaveMessageRoute = '/recordings'; +internals.kMenuMessage = 'Para dejar un mensaje, presiona 1. ' + + 'Para escuchar un mensaje al azar, presiona 2. ' + + 'Para escuchar un mensaje específico, presioan 3.'; // the message that will be shown +internals.kMenuInvalidResponseMessage = 'No entendí... Volviendo al menu principal.'; // invalid selection message +internals.kMenuOptions = { + leaveMessage: 1, + listenToRandomMessage: 2, + listenToSpecificMessage: 3 +}; // the menu options + +/** + * Handles the HTTP requests for the main menu + * + * @class MainMenuController + */ +module.exports = internals.MainMenuController = class MainMenuController { + + /** + * Serves the menu + * + * @function serveMenu + * @memberof MainMenuController + * @instance + * @return {generator} a koa compatible handler generator function + */ + serveMenu() { + + return function * () { + + const response = new Twilio.TwimlResponse(); + + // the action will default to post in the same URL, so no change + // required there. + response.gather({ + timeout: internals.kMenuTimeout, + numDigits: internals.kMenuDigits + }, function nestedHandler() { + + this.say(internals.kMenuMessage, { language: internals.kMenuLanguage }); + }).say(internals.kTimeoutMessage, { language: internals.kMenuLanguage }); + + this.type = internals.kContentType; + this.body = response.toString(); + }; + } + + /** + * Parses the selected main menu response + * + * @function parseMenuSelection + * @memberof MainMenuController + * @instance + * @return {generator} a koa compatible handler generator function + */ + parseMenuSelection() { + + return function * () { + + const menuSelection = parseInt(this.request.body.Digits); + + const response = new Twilio.TwimlResponse(); + + if (menuSelection === internals.kMenuOptions.leaveMessage) { + response.redirect(internals.kLeaveMessageRoute, { method: 'GET' }); + } + else if (menuSelection === internals.kMenuOptions.listenToRandomMessage) { + response.redirect(internals.kRandomMessageRoute, { method: 'GET' }); + } + else if (menuSelection === internals.kMenuOptions.listenToSpecificMessage) { + response.redirect(internals.kRecordingMenuRoute, { method: 'GET' }); + } + else { + response.say(internals.kMenuInvalidResponseMessage, { language: internals.kMenuLanguage }) + .redirect(internals.kMainMenuRoute, { method: 'GET' }); + } + + this.type = internals.kContentType; + this.body = response.toString(); + }; + } +}; diff --git a/lib/controllers/recording_menu.js b/lib/controllers/recording_menu.js new file mode 100644 index 0000000..55f2836 --- /dev/null +++ b/lib/controllers/recording_menu.js @@ -0,0 +1,82 @@ +'use strict'; + +const Twilio = require('twilio'); + +const internals = {}; + +internals.kMenuTimeout = 10; // timeout in seconds +internals.kContentType = 'application/xml'; // The content type used to respond +internals.kMenuLanguage = 'es-mx'; // the language to use +internals.kMainMenuRoute = '/menus/main'; +internals.kListenMessageRoute = '/recordings/'; +internals.kMenuMessage = 'Escribe el numero de mensaje y presiona gato para terminar.'; +internals.kTimeoutMessage = 'Bueno... volviendo al menú principal.'; +internals.kMenuInvalidResponseMessage = 'No entendí... Volviendo al menu principal.'; // invalid selection message + +/** + * Handles the HTTP requests for the recording menu + * + * @class RecordingMenuController + */ +module.exports = internals.RecordingMenuController = class RecordingMenuController { + + /** + * Serves the menu + * + * @function serveMenu + * @memberof RecordingMenuController + * @instance + * @return {generator} a koa compatible handler generator function + */ + serveMenu() { + + return function * () { + + const response = new Twilio.TwimlResponse(); + + // the action will default to post in the same URL, so no change + // required there. + response.gather({ + timeout: internals.kMenuTimeout + }, function nestedHandler() { + + this.say(internals.kMenuMessage, { language: internals.kMenuLanguage }); + }) + .say(internals.kTimeoutMessage, { language: internals.kMenuLanguage }) + .redirect(internals.kMainMenuRoute, { method: 'GET' }); + + this.type = internals.kContentType; + this.body = response.toString(); + }; + } + + /** + * Parses the selected recording id + * + * @function parseMenuSelection + * @memberof RecordingMenuController + * @instance + * @return {generator} a koa compatible handler generator function + */ + parseMenuSelection() { + + return function * () { + + const messageId = parseInt(this.request.body.Digits); + + const response = new Twilio.TwimlResponse(); + + if (messageId) { + response.redirect(`${internals.kListenMessageRoute}${messageId}`, { method: 'GET' }); + } + else { + response.say(internals.kMenuInvalidResponseMessage, { language: internals.kMenuLanguage }) + .redirect(internals.kMainMenuRoute, { method: 'GET' }); + } + + this.type = internals.kContentType; + this.body = response.toString(); + }; + } +}; + diff --git a/lib/controllers/recordings.js b/lib/controllers/recordings.js new file mode 100644 index 0000000..591dbd5 --- /dev/null +++ b/lib/controllers/recordings.js @@ -0,0 +1,166 @@ +'use strict'; + +const Joi = require('joi'); +const Pify = require('pify'); +const Redis = require('redis'); +const Twilio = require('twilio'); + +const internals = {}; + +internals.kContentType = 'application/xml'; // The content type used to respond +internals.kLanguage = 'es-mx'; // the language to use +internals.kMaxMessageLength = 30; // max message length in seconds +internals.kIdDateFormat = 'YYMMDDHHmmssSSS'; // derive ids from current date. 15 digits. +internals.kRecordingsSet = 'recordings'; +internals.kRecordMessage = 'Graba tu mensaje despues del bip. ' + + 'Presiona cualquier tecla para finalizar tu mensaje. '; // the recording message +internals.kConfirmationMessage = 'Gracias. Tu mensaje es el número: '; +internals.kNotFoundMessage = 'Mensaje no encontrado. Adiós!'; + +internals.kRecordingSchema = Joi.object().keys({ + url: Joi.string().required() +}); + +/** + * Handles the HTTP requests for the recording menu + * + * @class RecordingsController + * @param {DeadDrop.tConfiguration} config The configuration to + * initialize. + */ +module.exports = internals.RecordingsController = class RecordingsController { + constructor(config) { + + this._redis = Redis.createClient(config.redis); + + // Log an error if it happens. + this._redis.on('error', (err) => { + + console.error(err); + }); + } + + /** + * Start recording process + * + * @function startRecording + * @memberof RecordingsController + * @instance + * @return {generator} a koa compatible handler generator function + */ + startRecording() { + + return function * () { + + const response = new Twilio.TwimlResponse(); + + // the action will default to post in the same URL, so no change + // required there. + response.say(internals.kRecordMessage, { language: internals.kLanguage }) + .record({ + maxLength: internals.kMaxMessageLength + }); + + this.type = internals.kContentType; + this.body = response.toString(); + }; + } + + /** + * Saves the recording for later use + * + * @function saveRecording + * @memberof RecordingsController + * @instance + * @return {generator} a koa compatible handler generator function + */ + saveRecording() { + + const self = this; + + return function * () { + + const zadd = Pify(self._redis.zadd.bind(self._redis)); + + const response = new Twilio.TwimlResponse(); + + const id = Date.now().toString().substr(2,10); + const url = this.request.body.RecordingUrl; + const separatedId = id.split('').join('. '); + const recording = { + url + }; + + yield self._validate(recording).catch((err) => { + + this.throw(err.message, 422); + }); + + // Add to ordered set for quick fetches, and set for random fetches + yield zadd(internals.kRecordingsSet, parseInt(id), url); + + response.say(`${internals.kConfirmationMessage}${separatedId}`, { language: internals.kLanguage }); + + this.type = internals.kContentType; + this.body = response.toString(); + }; + } + + /** + * Gets a recording. + * + * @function getRecording + * @memberof RecordingsController + * @instance + * @return {generator} a koa compatible handler generator function + */ + getRecording() { + + const self = this; + + return function * (id) { + + id = parseInt(id); + + const zcard = Pify(self._redis.zcard.bind(self._redis)); + const zrange = Pify(self._redis.zrange.bind(self._redis)); + const zscore = Pify(self._redis.zscore.bind(self._redis)); + const zrangebyscore = Pify(self._redis.zrangebyscore.bind(self._redis)); + + const response = new Twilio.TwimlResponse(); + let location = null; + + if (!id) { + const maxNumber = yield zcard(internals.kRecordingsSet); + const index = Math.floor(Math.random() * maxNumber); // get random between 0 and cardinality + location = yield zrange(internals.kRecordingsSet, index, index); + } + else { + location = yield zrangebyscore(internals.kRecordingsSet, id, id); + } + + if (location && location.length > 0) { + if (!id) { + id = yield zscore(internals.kRecordingsSet, location[0]); + } + + const separatedId = id.toString().split('').join('. '); + response.play(location[0]).say(separatedId, { language: internals.kLanguage }); + } + else { + response.say(internals.kNotFoundMessage, { language: internals.kLanguage }); + } + + this.type = internals.kContentType; + this.body = response.toString(); + }; + } + + // Validates the post schema + + _validate(post) { + + const validate = Pify(Joi.validate.bind(Joi)); + return validate(post, internals.kRecordingSchema); + } +}; diff --git a/lib/dead_drop.js b/lib/dead_drop.js index 1ea86e8..6f97645 100644 --- a/lib/dead_drop.js +++ b/lib/dead_drop.js @@ -1,8 +1,13 @@ 'use strict'; const Koa = require('koa'); +const KoaBodyParser = require('koa-bodyparser'); const KoaRoute = require('koa-route'); +const MainMenuController = require('./controllers/main_menu'); +const RecordingMenuController = require('./controllers/recording_menu'); +const RecordingsController = require('./controllers/recordings'); + const internals = {}; /** @@ -42,60 +47,57 @@ module.exports = internals.DeadDrop = class DeadDrop { this._app = Koa(); - this._app.use(KoaRoute.get('/menus/main', function * () { - - this.body = 'I will return the main menu.'; - })); - - this._app.use(KoaRoute.post('/menus/main', function * () { + this._app.use(KoaBodyParser()); - this.body = 'I will parse the main menu.'; - })); + this._initializeMainMenuRoutes(); + this._initializeRecordingMenuRoutes(); + this._initializeRecordingsRoutes(); - this._app.use(KoaRoute.get('/menus/recording', function * () { + this._app.use(function * () { - this.body = 'I will return the select recording menu.'; - })); + this.body = 'How did you get here? Shoo!'; + }); - this._app.use(KoaRoute.post('/menus/recording', function * () { + } - this.body = 'I will parse the select recording menu.'; - })); + // Starts listening - this._app.use(KoaRoute.get('/recordings', function * () { + _startServer() { - this.body = 'I will initiate recording process'; - })); + this._app.listen(this.port); + } - this._app.use(KoaRoute.post('/recordings', function * () { + // Initializes the main menu routes. - this.body = 'I will create a new recording'; - })); + _initializeMainMenuRoutes() { - this._app.use(KoaRoute.get('/recordings/:id', function * (id) { + const mainMenuController = new MainMenuController(); - id = parseInt(id); + this._app.use(KoaRoute.get('/menus/main', mainMenuController.serveMenu())); + this._app.use(KoaRoute.post('/menus/main', mainMenuController.parseMenuSelection())); + } - if (id === 0) { - this.body = 'I will return a random recording'; - } - else { - this.body = 'I will return a specific recording'; - } - })); + // Initializes the recording menu routes. - this._app.use(function * () { + _initializeRecordingMenuRoutes() { - this.body = 'hello, world'; - }); + const recordingMenuController = new RecordingMenuController(); + this._app.use(KoaRoute.get('/menus/recording', recordingMenuController.serveMenu())); + this._app.use(KoaRoute.post('/menus/recording', recordingMenuController.parseMenuSelection())); } - // Starts listening + // Initializes the recordings routes. - _startServer() { + _initializeRecordingsRoutes() { - this._app.listen(this.port); + const recordingsController = new RecordingsController({ + redis: this.redis + }); + + this._app.use(KoaRoute.get('/recordings', recordingsController.startRecording())); + this._app.use(KoaRoute.post('/recordings', recordingsController.saveRecording())); + this._app.use(KoaRoute.get('/recordings/:id', recordingsController.getRecording())); } // Prints the banner. |