diff --git a/.gitignore b/.gitignore index 0beef0c..136e8af 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ subway.zip .env downloads .vscode +dynamodb_local diff --git a/handlers/store-favorite-line.js b/handlers/store-favorite-line.js new file mode 100644 index 0000000..c979785 --- /dev/null +++ b/handlers/store-favorite-line.js @@ -0,0 +1,14 @@ +const fetchMTAStatus = require('../services/fetch-mta-status.js'); +const closestLineMatcher = require('../utilities/closest-line-matcher.js'); + +module.exports = function() { + let heardNameGroup = this.event.request.intent.slots.subwayLineOrGroup.value; + + if(!nameGroup) { this.emit(':tell', "Sorry, I didn't hear a subway line or group I recognized") }; + + fetchMTAStatus(statuses => { + let closestLine = closestLineMatcher(statuses, 'nameGroup', heardNameGroup); + + this.emit('tell', `Your favorite line is ${closestLine.nameGroup}`); + }); +} diff --git a/index.js b/index.js index 240be2f..ca33394 100644 --- a/index.js +++ b/index.js @@ -5,35 +5,27 @@ const uuidv4 = require("uuid/v4"); var env = process.env.NODE_ENV || 'development'; const logRequests = process.env.LOG_REQUESTS === 'true'; +const applicationId = process.env.APPLICATION_ID; if(env !== 'production') { require('dotenv').load({ path: '.env.' + env }); } -var applicationId = process.env.APPLICATION_ID; -var mtaStatusURL = process.env.MTA_STATUS_URL; +const _ = require('lodash'); +const Alexa = require('alexa-sdk'); -var _ = require('lodash'); -var levenshtein = require('fast-levenshtein'); -var Alexa = require('alexa-sdk'); -var currentMTAStatus = require('./current-mta-status.js'); -var statusToSpeech = require('./status-to-speech.js'); +const statusToSpeech = require('./services/status-to-speech.js'); +const fetchMTAStatus = require('./services/fetch-mta-status.js'); +const closestLineMatcher = require('./utilities/closest-line-matcher.js'); -var fetchStatus = function(callback) { - return fetch(mtaStatusURL).then(function(response) { - return response.text() - }).then(function(body) { - currentMTAStatus(body, callback); - }); -}; +// Handlers +const storeFavoriteLineHandler = require('./handlers/store-favorite-line.js'); const affectedServiceStatusesBuilder = (statuses) => { - let notGoodService = status => status.status !== 'GOOD SERVICE'; - - let affectedServices = statuses - .filter(notGoodService) + let withServiceIssues = status => status.status !== 'GOOD SERVICE'; - return _(affectedServices) + return _(statuses) + .filter(withServiceIssues) .groupBy('status') .map((lines, status, collection) => { lines = lines.map(status => status.nameGroup); @@ -42,7 +34,7 @@ const affectedServiceStatusesBuilder = (statuses) => { }; var fullStatusUpdateHandler = function() { - fetchStatus(statuses => { + fetchMTAStatus(statuses => { let affectedServiceStatuses = affectedServiceStatusesBuilder(statuses); if(affectedServiceStatuses.length === 0) { @@ -58,22 +50,13 @@ var handlers = { statusOfLine: function () { var self = this; var nameGroup = self.event.request.intent.slots.subwayLineOrGroup.value; - var closestStatus = null; var badQueryResponse = function() { self.emit(':tell', "Sorry, I didn't hear a subway line I understand"); } if(nameGroup) { - fetchStatus(function(statuses) { - if(nameGroup.length > 1) { - closestStatus = _.minBy(statuses, function(status) { - return levenshtein.get(status.nameGroup, nameGroup.toUpperCase()); - }); - } else { - closestStatus = _.find(statuses, function(status) { - return status.nameGroup.search(nameGroup.toUpperCase()) !== -1; - }); - } + fetchMTAStatus(function(statuses) { + let closestStatus = closestLineMatcher(statuses, 'nameGroup', nameGroup); if(closestStatus) { if(closestStatus.description) { @@ -92,11 +75,12 @@ var handlers = { }, fullStatusUpdate: fullStatusUpdateHandler, - Unhandled: fullStatusUpdateHandler + storeFavoriteLine: storeFavoriteLineHandler, + Unhandled: fullStatusUpdateHandler, }; exports.flashBriefingHandler = (event, context, callback) => { - fetchStatus(statuses => { + fetchMTAStatus(statuses => { let affectedServiceStatuses = affectedServiceStatusesBuilder(statuses); let message; diff --git a/intents.json b/intents.json index 1a49a27..435972b 100644 --- a/intents.json +++ b/intents.json @@ -11,6 +11,15 @@ { "intent": "fullStatusUpdate", "slots": [] + }, + { + "intent": "storeFavoriteLine", + "slots":[ + { + "name": "subwayLineOrGroup", + "type": "SUBWAY_LINE_OR_GROUP" + } + ] } ] } diff --git a/current-mta-status.js b/parsers/current-mta-status.js similarity index 79% rename from current-mta-status.js rename to parsers/current-mta-status.js index 38db4d2..0aa8a93 100644 --- a/current-mta-status.js +++ b/parsers/current-mta-status.js @@ -1,5 +1,5 @@ -var parseXML = require('xml2js').parseString; -var serviceDetailsCleaner = require('./service-details-cleaner'); +const parseXML = require('xml2js').parseString; +const serviceDetailsCleaner = require('./service-details-cleaner'); module.exports = function(mtaStatusXML, callback) { parseXML(mtaStatusXML, { normalize: true, trim: true }, function(error, results) { diff --git a/parsers/service-details-cleaner.js b/parsers/service-details-cleaner.js new file mode 100644 index 0000000..7befb85 --- /dev/null +++ b/parsers/service-details-cleaner.js @@ -0,0 +1,12 @@ +const sanitize = require('sanitize-html'); +const MATCHERS = { + br: /\s*/g +} + +module.exports = function(detailsText) { + let text = detailsText.replace(MATCHERS.br, '\n'); // BRs to newline + + return sanitize(text, { + allowedTags: [] + }); +}; diff --git a/service-details-cleaner.js b/service-details-cleaner.js deleted file mode 100644 index ae45b64..0000000 --- a/service-details-cleaner.js +++ /dev/null @@ -1,12 +0,0 @@ -var sanitize = require('sanitize-html'); -var MATCHERS = { - br: /\s*/g -} - -module.exports = function(detailsText) { - var text = detailsText.replace(MATCHERS.br, '\n'); // BRs to linebreaks - - return sanitize(text, { - allowedTags: [] - }); -}; diff --git a/services/fetch-mta-status.js b/services/fetch-mta-status.js new file mode 100644 index 0000000..1adc8d8 --- /dev/null +++ b/services/fetch-mta-status.js @@ -0,0 +1,8 @@ +const MTA_STATUS_URL = process.env.MTA_STATUS_URL; +const currentMTAStatus = require('../parsers/current-mta-status.js'); + +module.exports = function(callback) { + return fetch(MTA_STATUS_URL) + .then(response => response.text()) + .then(body => currentMTAStatus(body, callback)); +}; diff --git a/status-to-speech.js b/services/status-to-speech.js similarity index 100% rename from status-to-speech.js rename to services/status-to-speech.js diff --git a/tests/index-test.js b/tests/index-test.js index ee0883c..af850f5 100644 --- a/tests/index-test.js +++ b/tests/index-test.js @@ -53,7 +53,7 @@ test.serial('handling "ask subway status to check on the line?" an fetchMock.restore(); }); -test.serial('handling "ask subway status to check on the line?" and the service the service is good', async t => { +test.serial('handling "ask subway status to check on the line?" and the service is good', async t => { t.plan(2); const statusOfLineEvent = JSON.parse(fs.readFileSync(process.cwd() + '/tests/fixtures/events/status-of-line.json')); @@ -97,6 +97,7 @@ test.serial('handling "ask subway status for an update and there are bad service .expectSucceed(r => r); let speechMarkup = result.response.outputSpeech.ssml; + t.true(speechMarkup.search("1-2-3, B-D-F-M, J-Z, N-Q-R") !== -1); t.true(speechMarkup.search('A-C-E') !== -1); t.true(speechMarkup.search('Good service on all other lines. ') !== -1); diff --git a/tests/service-details-cleaner-test.js b/tests/service-details-cleaner-test.js index 273fc54..424f922 100644 --- a/tests/service-details-cleaner-test.js +++ b/tests/service-details-cleaner-test.js @@ -2,7 +2,7 @@ import test from 'ava'; import fs from 'fs'; import { parseString } from 'xml2js' -import { default as serviceDetailsCleaner } from '../service-details-cleaner.js'; +import { default as serviceDetailsCleaner } from '../parsers/service-details-cleaner.js'; test('removes all linebreaks', t => { t.plan(1); diff --git a/tests/status-to-speech-test.js b/tests/status-to-speech-test.js index 3e31703..39d1213 100644 --- a/tests/status-to-speech-test.js +++ b/tests/status-to-speech-test.js @@ -1,6 +1,6 @@ import test from 'ava'; -import statusToSpeech from '../status-to-speech.js'; +import statusToSpeech from '../services/status-to-speech.js'; test('Given a service status, it builds the right speech"', t => { t.plan(4); diff --git a/utilities/closest-line-matcher.js b/utilities/closest-line-matcher.js new file mode 100644 index 0000000..4ac7429 --- /dev/null +++ b/utilities/closest-line-matcher.js @@ -0,0 +1,17 @@ +const _ = require('lodash'); +const levenshtein = require('fast-levenshtein'); + +module.exports = function(statuses, key, potentialValue) { + let closetStatus; + + if(potentialValue.length > 1) { + closestStatus = _.minBy(statuses, function(status) { + return levenshtein.get(status[key], potentialValue.toUpperCase()); + }); + } else { + closestStatus = _.find(statuses, function(status) { + return status[key].search(potentialValue.toUpperCase()) !== -1; + }); + } + return closestStatus; +} diff --git a/utterances.txt b/utterances.txt index f9d0b0d..45143fa 100644 --- a/utterances.txt +++ b/utterances.txt @@ -5,3 +5,9 @@ statusOfLine what is the status of {subwayLineOrGroup} fullStatusUpdate for a service update fullStatusUpdate for a full service update fullStatusUpdate for an update +storeFavoriteLine to track the {subwayLineOrGroup} +removeFavoriteLine to forget the {subwayLineOrGroup} +resetFavoriteLine to forget all my tracked lines +checkFavoriteLines to check my favorite lines +checkFavoriteLines what is the status of my favorite lines? +checkFavoriteLines to check my commute