Linking with External APIs

While some Alexa skills are self-contained, many are actually just a front-end user interface to a larger backend application. This is similar to how many React or Angular applications are just front-ends that integrate with a backend application.

The Star Port 75 Travel skill may provide a lot of functionality on its own, but ultimately it is a user interface to what could be a much larger backend travel planning system. Therefore, we will need to connect our skill to the API of a backend system that handles the actual booking.

Account linking is a means by which an Alexa skill can connect to an API on behalf of a user, using OAuth 2[23] for security between the skill and the API. It involves registering the skill as a client application with the backend API and configuring the skill with the OAuth 2 client details so that Alexa can conduct the authorization process to obtain permission for the skill to make requests to the API.

You do not need to fully understand OAuth 2 to take advantage of account linking, but it might be helpful for you to become familiar with a few of the essentials before diving into account linking.

Rather than take time away from working directly on our Alexa skill while we develop a backend booking system, let’s use an existing API to act as a surrogate booking system. Specifically, we’ll ask Google Calendar’s API to stand in as our travel planning backend service. A travel event is, after all, largely concerned with dates on a calendar, so Google Calendar will do just fine for our needs. The first thing we’ll need to do is configure client credentials for our skill to access the Google Calendar API.

Configuring the Skill as an OAuth 2 Client

As with any OAuth 2 client, an Alexa skill involved in account linking must be configured with details about how to obtain an access token from the remote service. Generally, these are the key pieces of information needed:

  • Client ID—An identifier for the client

  • Client Secret—A secret string that acts as a password for the client

  • Authorization URL—A URL to a web page where the user will be prompted to grant permission to the client to access the user’s data

  • Access Token URL—A URL for a REST endpoint from which the client will obtain an access token that will be used on requests to the API

No matter what backend service you are connecting with, the client ID and secret are typically assigned when registering the client with the service. The authorization and access token URLs are usually available from the service’s documentation and often displayed along with the client ID and secret.

For Google’s Calendar API, you’ll need to register your skill as a client application and create credentials. Follow Google’s instructions for setting up an OAuth2 client for Google Calendar,[24] and you’ll receive a client ID and secret that you can use to setup account linking for the Star Port 75 Travel skill.

To setup account linking, create a JSON file that describes the client details for the backend service. Because the file will contain client secrets and only be used temporarily, the name of the file doesn’t matter and you shouldn’t commit it to source code control. As for the contents of the file, it should look a little something like this:

 {
 "accountLinkingRequest"​: {
 "clientId"​: ​"518123456789-00mcp70e9c.apps.googleusercontent.com"​,
 "clientSecret"​: ​"emqj0OpiRkfx5xKZ1RgqKRPi"​,
 "authorizationUrl"​: ​"https://accounts.google.com/o/oauth2/v2/auth"​,
 "accessTokenUrl"​: ​"https://www.googleapis.com/oauth2/v4/token"​,
 "scopes"​: [
 "https://www.googleapis.com/auth/calendar.events"
  ],
 "type"​: ​"AUTH_CODE"​,
 "accessTokenScheme"​: ​"HTTP_BASIC"​,
 "domains"​: [],
 "skipOnEnablement"​: ​true
  }
 }

The actual client ID and secret shown here are fake; you shouldn’t try to use them. But the authorization and access token URLs are the actual URLs you’ll need. As for the rest of the properties, they are as follows:

  • scopes—The scope of permission(s) that a connected skill will have. In this case, the scope allows the skill to create new events on the user’s calendar.

  • type—Alexa account linking supports two kinds of authorization: authorization code grant and implicit grant (see the OAuth 2 specification for details). In this case, we’re using authorization code grant.

  • accessTokenScheme—When requesting an access token, a client may pass its own credentials, either using HTTP Basic authentication or as form parameters. For the Google Calendar API, we’re specifying HTTP Basic authentication of client credentials.

  • domain—A list of domains from which the authorization page may fetch data from. For example, if the authorization page loads images from a domain different from that of the authorization URL, then you must specify that domain here.

  • skipOnEnablement—When set to true, enables a skill for optional account linking. This allows a user to enable and use your skill without linking accounts first. If set to false, the user will be prompted to link their account upon enabling the skill.

Now that we’ve defined the account linking metadata, we can use it to enable account linking for our skill. We’ll use Alexa’s Skill Management API (SMAPI) to setup account linking. SMAPI is a REST API that offers several endpoints for managing the finer details of a skill. Although you can make requests to SMAPI using any HTTP client you like, the ask smapi command is the most convenient way to interact with SMAPI, offering several subcommands that map to the REST API’s endpoints. The responses to all ask smapi subcommands are in JSON format.

For purposes of setting up account linking for our skill, we’ll use the update-account-linking-info. Assuming that we saved the account linking metadata in a file named linking.json, we can enable account linking like this:

 $ ​​ask​​ ​​smapi​​ ​​update-account-linking-info​​ ​​
  ​​--skill-id=amzn1.ask.skill.28e3f37-2b4-4b5-849-bcf4f2e081​​ ​​
  ​​--account-linking-request=file:linking.json

We must also specify the skill ID with --skill-id. The skill ID shown here is fake, so you’ll need to use the actual skill ID for your skill. If you’re not sure what the skill ID is, you can find it on the home page of the Alexa Developer Console[25] by clicking the “View Skill ID” link under the skill’s name. Alternatively, if you have deployed the skill at least once, you can find it in the .ask/ask-states.json file, by searching for the skillId property:

 $ ​​more​​ ​​.ask/ask-states.json​​ ​​|​​ ​​grep​​ ​​"skillId"
  "skillId": "amzn1.ask.skill.28e3f37-2b4-4b5-849-bcf4f2e081",

Finally, we supply the account linking specification through the --account-linking-request parameter. This parameter can accept raw JSON or, if prefixed with “file:” as shown here, reference a JSON file.

Now that we have enabled account linking for our skill, we can delete the account linking metadata file. We won’t be needing it again and because it contains our application’s credentials, it shouldn’t be checked into source code control. If we ever need to recreate it for any reason, we’ll need to lookup the credentials from Google’s developer site. But for now we’re ready to start taking advantage of account linking to post our planned trips to our users’ calendars.

Making Authorized Requests

When a user first uses our skill, they will not have yet linked their Google Calendar to the Star Port 75 Travel skill. That means that if they start to plan a trip, we won’t be able to save their trip to their calendar. And since we’re trying Google Calendar as a surrogate backend travel system, that means that our skill will not be able to do its job of actually booking the trip for the user. Therefore, we need to stop the user from planning a trip until they’ve linked their account.

To do that, we’ll create a new intent handler for ScheduleTripIntent. But unlike our other intent handlers, this one will only be triggered if the user hasn’t linked their Google Calendar yet. This new intent handler will be created in a file named ScheduleTripIntentHandler_Link.js and will look like this:

 const​ Alexa = require(​'ask-sdk-core'​);
 
 const​ ScheduleTripIntentHandler_Link = {
  canHandle(handlerInput) {
 return​ Alexa.getRequestType(handlerInput.requestEnvelope)
  === ​'IntentRequest'
  && Alexa.getIntentName(handlerInput.requestEnvelope)
  === ​'ScheduleTripIntent'
  && !Alexa.getAccountLinkingAccessToken(handlerInput.requestEnvelope);
  },
  handle(handlerInput) {
 const​ linkText = handlerInput.t(​'LINK_MSG'​);
 
 return​ handlerInput.responseBuilder
  .speak(linkText)
» .withLinkAccountCard()
  .getResponse();
  }
 };
 
 module.exports=ScheduleTripIntentHandler_Link;

As you can see, the canHandle() function is quite similar to the other two intent handlers we’ve written for ScheduleTripIntent. It checks that the request type is an intent request and that the intent name is ScheduleTripIntent. But then it also tried to fetch the account linking token from the request. If the user has linked their account, the account linking token should have a value; if not, then that means that this intent handler needs to go to work to prompt the user to link their Google Calendar to the skill.

The handle() function is rather simple. The most significant line in handle() is the call to the response builder’s withLinkAccountCard() function. This simple line will cause Alexa to send an account linking card to the user’s companion application. From there, the user may link their account before trying to plan a trip.

When creating the response, the handle() function also includes a message that Alexa will speak to the user, asking them to link their Google Calendar account to the skill. That message is defined in languageStrings.js with the key “LINK_MSG”:

 module.exports = {
  en: {
  translation: {
  ...
  LINK_MSG: ​"You'll need to link the Star Port "​ +
 "75 Travel skill with your Google Calendar so "​ +
 "that I can schedule your trip. I've added a "​ +
 "card in the Alexa app to help you with that."​,
  ...
  }
  }
 }

Since this is a new intent handler, we’ll need to remember to register it with the skill builder in index.js:

 const​ ScheduleTripIntentHandler_Link =
  require(​'./ScheduleTripIntentHandler_Link'​);
 
 ...
 
 exports.handler = Alexa.SkillBuilders.custom()
  .addRequestHandlers(
  HelloWorldIntentHandler,
» ScheduleTripIntentHandler_Link,
  ScheduleTripIntentHandler,
  ScheduleTripIntentHandler_InProgress,
  StandardHandlers.LaunchRequestHandler,
  StandardHandlers.HelpIntentHandler,
  StandardHandlers.CancelAndStopIntentHandler,
  StandardHandlers.FallbackIntentHandler,
  StandardHandlers.SessionEndedRequestHandler,
  StandardHandlers.IntentReflectorHandler
  )
  .addErrorHandlers(
  StandardHandlers.ErrorHandler)
  .withApiClient(​new​ Alexa.DefaultApiClient())
  .addRequestInterceptors(
  LocalisationRequestInterceptor)
  .lambda();

Notice that this new intent handler is registered before the other two intent handlers for ScheduleTripIntent. That’s because it needs to consider the request before the others and make sure that the user has linked their account before even attempting to walk the user through the trip planning process.

Now, assuming that the user has linked their Google Calendar with our skill, we can use the account linking access token to make requests to the Google Calendar API. More specific to our needs, we can post a new calendar event that represents a planned trip. The following module encapsulates the action of saving a trip to the user’s calendar:

 const​ Alexa = require(​'ask-sdk-core'​);
 const​ fetch = require(​'node-fetch'​);
 
 module.exports = {
 async​ saveTrip(handlerInput, destination, departureDate, returnDate) {
»const​ accessToken =
» Alexa.getAccountLinkingAccessToken(handlerInput.requestEnvelope);
 const​ payload = {
  summary: ​`Trip to ​${destination}​`​,
  description: ​`Trip to ​${destination}​`​,
  start: { ​'date'​: departureDate },
  end: { ​'date'​: addOneToDate(returnDate) }
  };
 
 await​ fetch(
 'https://www.googleapis.com/calendar/v3/calendars/primary/events'​,
  {
  method: ​'POST'​,
  body: JSON.stringify(payload),
  headers: {
 'Accept'​: ​'application/json'​,
 'Content-type'​: ​'application/json'​,
»'Authorization'​: ​'Bearer '​ + accessToken
  }
  });
  }
 };
 
 function​ addOneToDate(yyyyMMddString){
 var​ asDate = ​new​ Date(yyyyMMddString);
  asDate.setDate(asDate.getDate() + 1);
 return​ [ asDate.getFullYear(),
  (​'0'​ + (asDate.getMonth() + 1)).slice(-2),
  (​'0'​ + asDate.getDate()).slice(-2)]
  .join(​'-'​);
 }

By keeping all of the Google Calendar specific code in this module, we avoid cluttering up the code in ScheduleTripIntentHandler.js. It will also make it easy to swap out for a more realistic travel system later if we decide to do so.

This module starts by calling Alexa.getAccountLinkingAccessToken() to fetch the account linking access token from the handlerInput. Because the ScheduleTripIntentHandler_Link intent handler should’ve prevented the user from getting this far without having linked their Google Calendar, we can rest assured that the access token is non-empty.

Next, the request payload is constructed. The only fields required are the calendar event’s summary, description, and start and end dates. Because the end date is non-inclusive, the end property is set using a function defined lower in the module to add a day. This way the event will appear in the calendar, spanning from the start date up to and including the end date.

Finally, we use the node-fetch module to POST the payload to the Google Calendar API. Pay specific attention to the Authorization header, which we set to “Bearer” followed by the account linking access token. Without this header, the request will fail for lack of permission.

Notice that the call to fetch() is prefixed with await. That’s because fetch() is performed asynchronously and returns a Javascript promise. Without await, it would return the promise immediately. The await keyword causes our code to wait for the request to complete before moving on. And, because we’re using await, the saveTrip() function is itself marked as async to indicate that it performs an asynchronous function.

Since we’re using the node-fetch module to make HTTP requests, we’ll need to be sure to install it using npm:

 $ ​​npm​​ ​​install​​ ​​--prefix​​ ​​./lambda​​ ​​node-fetch

The last thing we need to do is require the new GoogleCalendarTripSaver module in the ScheduleTripIntentHandler module and use it to save a completely defined trip:

 const​ Alexa = require(​'ask-sdk-core'​);
 const​ TripSaver = require(​'./GoogleCalendarTripSaver'​);
 const​ getResolvedSlotValue = require(​'./Helpers'​);
 const​ ScheduleTripIntentHandler = {
  canHandle(handlerInput) {
 return​ Alexa.getRequestType(
  handlerInput.requestEnvelope) === ​'IntentRequest'
  && Alexa.getIntentName(
  handlerInput.requestEnvelope) === ​'ScheduleTripIntent'
  && Alexa.getDialogState(handlerInput.requestEnvelope) === ​'COMPLETED'​;
  },
 async​ handle(handlerInput) {
 const​ destination =
  getResolvedSlotValue(handlerInput.requestEnvelope, ​'destination'​);
 
 const​ departureDate =
  Alexa.getSlotValue(handlerInput.requestEnvelope, ​'departureDate'​);
 const​ returnDate =
  Alexa.getSlotValue(handlerInput.requestEnvelope, ​'returnDate'​);
 
 await​ TripSaver.saveTrip(
  handlerInput, destination, departureDate, returnDate);
 
 const​ speakOutput = handlerInput.t(​'SCHEDULED_MSG'​,
  { destination: destination });
 return​ handlerInput.responseBuilder
  .speak(speakOutput)
  .withShouldEndSession(​true​)
  .getResponse();
  },
 };
 
 module.exports=ScheduleTripIntentHandler;

Notice that the result of the require() call is assigned to a constant named TripSaver. Later, if we were to decide to replace Google Calendar with another backend module, we’d only need to change the module included. Everything else in ScheduleTripIntentHandler could remain the same.

Now that we have the new intent handler in place to check for an account linking access token, and a new module for saving a trip being used by ScheduleTripIntentHandler, it’s time to take them out for a test drive.

Testing Account Linking

We’re going to use ask dialog to try out our new account linking code. Since ask dialog enables us to interact with a deployed skill as if we were talking to an actual device, we’ll first need to deploy the skill by committing it to AWS CodeCommit (if Alexa-hosted):

 $ ​​git​​ ​​add​​ ​​.
 $ ​​git​​ ​​commit​​ ​​-m​​ ​​"Add account linking"
 $ ​​git​​ ​​push

Or, if the skill is hosted in AWS Lambda, then deploy using ask deploy:

 $ ​​ask​​ ​​deploy

Now let’s fire up ask dialog and try to plan a trip:

 $ ​​ask​​ ​​dialog​​ ​​--locale​​ ​​en-US
  User > open star port seventy five
  Alexa > Welcome back to Star Port 75 Travel, Craig! How can I help you?
  User > plan a trip to Jupiter
  Alexa > You'll need to link the Star Port 75 Travel skill with your
  Google Calendar so that I can schedule your trip. I've added
  a card in the Alexa app to help you with that.
  User >

As you can see, everything started out fine. In fact, the skill even greeted the user by name using the code we wrote earlier in this chapter. But then when we asked to plan a trip to Jupiter, it stopped and asked us to link the skill with Google Calendar.

Meanwhile, in the companion application, the account linking card was displayed:

images/integration/AccountLinkingCard.png

 

If we are going to successfully schedule a trip, then we’ll need to link with Google Calendar. If we click the “LINK ACCOUNT” button, we’ll be taken to Google’s authorization page (by way of the Google login page if we’re not already authenticated) and asked to permit Star Port 75 Travel access to our calendar. Once permitting access, we can try again:

 $ ​​ask​​ ​​dialog​​ ​​--locale​​ ​​en-US
  User > open star port seventy five
  Alexa > Welcome to Star Port 75 Travel. How can I help you?
  User > plan a trip to Jupiter
  Alexa > When do you want to depart?
  User > September 15th
  Alexa > When do you want to return?
  User > September 20th
  Alexa > I've got you down for a trip to Jupiter leaving on
  2019-09-15 and returning 2019-09-20. Is that correct?
  User > yes
  Alexa > Enjoy your trip to Jupiter!
 ---------- Skill Session Ended ----------
  User >

This time, it seems to have worked. Because we’ve linked accounts, the ScheduleTripIntentHandler_Link intent handler is satisfied and allows the trip planning flow to continue as normal. Once we’ve completed planning our trip to Jupiter, we should expect to see the trip on our calendar like this:

images/integration/JupiterTripCalendar.png

And now our trip to Jupiter is on the books. Time to start packing!

Unfortunately, it is very difficult to automatically test account linking in our skill. We can easily create a BST filter to set an account linking access token and use Nock to mock the request to Google’s Calendar API. but because BST doesn’t handle dialog flows, we’d also need to write even more filters to test each stage of the flow individually. So, for now, we’ll just rely on manual testing with ask dialog to prove that account linking works in our skill.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset