Jang-Soo Lee
Created November 30, 2016 © MIT

lirrbits

A quick and easy way to get the next train times for the Long Island Railroad

IntermediateWork in progress5 hours46
lirrbits

Things used in this project

Story

Read more

Code

index.js

JavaScript
Use in conjunction with dependencies found in package.json
'use strict';

var request = require('request');
var os = require('os');
/**
 * This sample demonstrates a skill built with the Amazon Alexa Skills Kit.
 * The Intent Schema, Custom Slots, and Sample Utterances for this skill, as well as
 * testing instructions are located at http://lirrbits.com/alexa
 */


//Start: Initialize cache, so that warm starts can take advantage of it being populated.
var cache;
var thinkIAmOk = true;
let apiKey = process.env.traintime_key;

function buildCache() {
  request("https://traintime.lirr.org/api/StationsAll?api_key=" + apiKey, function(err, response, body){
    if (err) {
      console.log('ran into error building cache');
      setTimeout(function() {buildCache(); }, 10);
      return;
    }
    cache = JSON.parse(body);
    console.log('cache set up');
  });
}
var none = buildCache();
//End: Initialize cache, so that warm starts can take advantage of it being populated.

// --------------- Helpers that build all of the responses -----------------------

function buildSpeechletResponse(title, output, repromptText, shouldEndSession) {
    return {
        outputSpeech: {
            type: 'PlainText',
            text: output,
        },
        card: {
            type: 'Standard',
            title: 'LIRRBits.com',
            text: `${output}`,
            image: {
              smallImageUrl: 'https://brucel.ee/smallLogoLB.png',
              largeImageUrl: 'https://brucel.ee/largeLogoLB.png'
            }
        },
        reprompt: {
          outputSpeech: {
            type: "PlainText",
            text: repromptText
          }
        },
        shouldEndSession,
    };
}

function buildResponse(sessionAttributes, speechletResponse) {
    return {
        version: '1.0',
        sessionAttributes,
        response: speechletResponse,
    };
}


// --------------- Functions that control the skill's behavior -----------------------

function getWelcomeResponse(callback) {
    // If we wanted to initialize the session to have some attributes we could add those here.
    const sessionAttributes = {};
    const cardTitle = 'Welcome';
    const speechOutput = 'Welcome to lir bits. What would you like to know?';

    callback(sessionAttributes,
        buildSpeechletResponse(cardTitle, speechOutput, 'You can ask lir bits about the next trains from one station to another, or to give station information for a given station.', false));
}

function handleSessionEndRequest(callback) {
    const cardTitle = 'Session Ended';
    const speechOutput = 'Have a nice day!';
    // Setting this to true ends the session and exits the skill.
    const shouldEndSession = true;

    callback({}, buildSpeechletResponse(cardTitle, speechOutput, null, shouldEndSession));
}

// --------------- Events -----------------------

/**
 * Called when the session starts.
 */
function onSessionStarted(sessionStartedRequest, session) {
    console.log(`onSessionStarted requestId=${sessionStartedRequest.requestId}, sessionId=${session.sessionId}`);
}

/**
 * Called when the user launches the skill without specifying what they want.
 */
function onLaunch(launchRequest, session, callback) {
    console.log(`onLaunch requestId=${launchRequest.requestId}, sessionId=${session.sessionId}`);

    // Dispatch to your skill's launch.
    getWelcomeResponse(callback);
}

/**
 * Given StationOne and StationTwo information in an intent, as well as an api
 * key, give either full, extended information about the next few trains, or
 * just the departure times.
 */
function getTimes(intent, full, callback) {
  if (!cache) {
    console.log('still warming...');
    setTimeout(function() {getTimes(intent, full, callback);}, 25);
    return;
  }

  //Request all stations from the API, so that we can map a station name to its
  //abbreviation
  var normalizedStationOne = intent.slots.StationOne.value && intent.slots.StationOne.value.toLowerCase() || "";
  var normalizedStationTwo = intent.slots.StationTwo.value && intent.slots.StationTwo.value.toLowerCase() || "";

  var stations = cache;
  var stationOneAbbr;
  var stationTwoAbbr;

  for (var i in stations.Stations) {
    cache[stations.Stations[i].ABBR] = stations.Stations[i].NAME;
    if (stations.Stations[i].NAME.toLowerCase() === normalizedStationOne){
      stationOneAbbr = stations.Stations[i].ABBR;
    }
    if (stations.Stations[i].NAME.toLowerCase() === normalizedStationTwo){
      stationTwoAbbr = stations.Stations[i].ABBR;
    }
  }
  if (!stationOneAbbr || !stationTwoAbbr) {
    //If we can't find the station, give the user information that we could
    //not find the station.
    callback({},
      buildSpeechletResponse("card", "I was unable to determine times from " + normalizedStationOne + " to " + normalizedStationTwo, 'reprompt', true));
  } else {
    //Otherwise, ask the API for the metadata around the next few trains from
    //the first station to the second
    request("https://traintime.lirr.org/api/TrainTime?api_key=" + apiKey + "&startsta=" + stationOneAbbr + "&endsta=" + stationTwoAbbr, function(err, response, body){
      if (err) {
        callback({},
          buildSpeechletResponse("card", "I ran into an error trying to determine times from " + normalizedStationOne + " to " + normalizedStationTwo, 'reprompt', true));
        return;
      }
      var outs = JSON.parse(body);
      var info = "";
      //Return up to five trips, to avoid overwhelming the user
      for (var k = 0; k < outs.TRIPS.length && k < 5; k++) {
        //Give the start time by looking at the first leg and the first stop
        info = info + outs.TRIPS[k].LEGS[0].STOPS[0].TIME + ", ";
        //User may not want the full set of information
        if (!full){
          continue;
        }
        //Peak and offpeak denote cost differences in ticket prices
        if (outs.TRIPS[k].PEAK_INDICATOR === 1) {
          info = info + " Peak, ";
        } else {
          info = info + " Off Peak, ";
        }
        let legs = outs.TRIPS[k].LEGS;
        //Check for whether one or more connections are needed, and add that
        //information to the response
        if (outs.TRIPS[k].LEGS.length !== 1) {
          for (var legIndex = 0; legIndex < outs.TRIPS[k].LEGS.length - 1; legIndex++) {
            let leg = legs[legIndex];
            info = info + " Connecting at " + cache[leg.STOPS[leg.STOPS.length - 1].STATION] + ", ";
          }
        }
        let finalLeg = legs[legs.length - 1];
        let finalStop = finalLeg.STOPS[finalLeg.STOPS.length - 1];
        info = info + finalStop.TIME + " Arrival. ";
      }
      if (info.length > 0) {
        info = info.substring(0, info.length - 2);
        info = "These are the next departures from " + normalizedStationOne + " to " + normalizedStationTwo + ": " + info ;
      } else {
        info = "I was unable to find information from " + normalizedStationOne + " to " + normalizedStationTwo;
      }
      callback({},
        buildSpeechletResponse("card", info, 'reprompt', true));

    });
  }
}

function getStationInformation(intent, callback) {
  if (!cache) {
    console.log('still warming...');
    setTimeout(function() {getStationInformation(intent, callback);}, 25);
    return;
  }
  var stations = cache;
  for (var i in stations.Stations) {
    if (stations.Stations[i].NAME.toLowerCase() === intent.slots.Station.value){
      let ticketMachine = stations.Stations[i].TICKETMACHINE;
      if (ticketMachine.indexOf('None') === 0) {
        ticketMachine = "No ticket machine information is listed for this station."
      }
      callback({},
        buildSpeechletResponse("card", "The station is located at: " + stations.Stations[i].LOCATION + ". " + ticketMachine, 'reprompt', true));
      return;
    }
  }
  callback({},
    buildSpeechletResponse("card", "I wasn't able to find station information for " + intent.slots.Station.value, 'reprompt', true));
}

/**
 * Called when the user specifies an intent for this skill.
 */
function onIntent(intentRequest, session, callback) {
    console.log(`onIntent requestId=${intentRequest.requestId}, sessionId=${session.sessionId}`);

    const intent = intentRequest.intent;
    const intentName = intentRequest.intent.name;

    // Dispatch to your skill's intent handlers
    console.log(intentName);
    if (intentName === 'GetStationInformation') {
      getStationInformation(intent, callback);
    } else if (intentName === 'GetFullTimesInformation') {
      getTimes(intent, true, callback);
    } else if (intentName === 'GetTimesInformation') {
      getTimes(intent, false, callback);
    } else if (intentName === 'AMAZON.HelpIntent') {
      callback({},
        buildSpeechletResponse('Help', 'You can ask lir bits about the next trains from one station to another, or to give station information for a given station.', 'What would you like to ask lir bits?', false));
    } else if (intentName === 'AMAZON.StopIntent' || intentName === 'AMAZON.CancelIntent') {
        handleSessionEndRequest(callback);
    } else {
        throw new Error('Invalid intent');
    }
}

/**
 * Called when the user ends the session.
 * Is not called when the skill returns shouldEndSession=true.
 */
function onSessionEnded(sessionEndedRequest, session) {
    console.log(`onSessionEnded requestId=${sessionEndedRequest.requestId}, sessionId=${session.sessionId}`);
    // Add cleanup logic here
}


// --------------- Main handler -----------------------

// Route the incoming request based on type (LaunchRequest, IntentRequest,
// etc.) The JSON body of the request is provided in the event parameter.
exports.handler = (event, context, callback) => {
    try {
        console.log(`event.session.application.applicationId=${event.session.application.applicationId}`);

        if (event.session.application.applicationId !== 'amzn1.ask.skill.b0ebfcb9-9323-4470-914f-d3e001a3e0ba') {
             callback('Invalid Application ID');
        }

        if (event.session.new) {
            onSessionStarted({ requestId: event.request.requestId }, event.session);
        }

        if (event.request.type === 'LaunchRequest') {
            onLaunch(event.request,
                event.session,
                (sessionAttributes, speechletResponse) => {
                    callback(null, buildResponse(sessionAttributes, speechletResponse));
                });
        } else if (event.request.type === 'IntentRequest') {
            onIntent(event.request,
                event.session,
                (sessionAttributes, speechletResponse) => {
                    callback(null, buildResponse(sessionAttributes, speechletResponse));
                });
        } else if (event.request.type === 'SessionEndedRequest') {
            onSessionEnded(event.request, event.session);
            callback();
        }
    } catch (err) {
        callback(err);
    }
};

package.json

JSON
dependencies!
{
  "name": "lirrbits-alexa",
  "version": "1.0.0",
  "description": "lirrbits-alexa",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "repository": {
    "type": "git",
    "url": "git+ssh://git@bitbucket.org/jangiegb/lirrbits-alexa.git"
  },
  "dependencies": {
    "request": "2.79.0"
  },
  "author": "jangie",
  "license": "ISC",
  "homepage": "https://bitbucket.org/jangiegb/lirrbits-alexa#readme"
}

Credits

Jang-Soo Lee

Jang-Soo Lee

1 project • 0 followers

Comments