Accessing a User’s Amazon Info

As people get to know each other, they learn and remember things about each other. We start by getting to know each others’ names. Then, when we greet each other in the future, we can be more personable and acknowledge each other by name. As we get to know someone better, we might learn their phone number or email address so that we can contact them later. As the relationship progresses, you may share your address with someone so that they can visit you or maybe send you a birthday or Christmas card.

Although Alexa isn’t human, it helps the users of our skill to feel comfortable talking with her if she’s able to speak with them on a personal level. If she greets someone by name when a skill is launched, it makes her seem more human and less like a machine. And if she knows useful information such as a user’s email address, then the skill can send the user information that they can refer to later when not interacting with the skill.

Alexa offers several services through which a skill can obtain several useful bits of information about a user:

  • The user’s name (first name or full name)
  • The user’s email address
  • The user’s phone/mobile number
  • The device’s address
  • The device’s geolocation

Even though these services and the data that they offer are available to any skill, you don’t need to worry that every Alexa skill knows too much about you. In order for a skill to use these services, they must provide an authorized access token in their requests to the skills. And the only way a skill is able to provide such an access token is if the user has explicitly granted permission to read that information.

In the Star Port 75 Travel skill, we’re going to leverage an Alexa-provided service to fetch the user’s name and greet them personally when they launch the skill. Before we can do that, however, we’ll need to first configure the skill to be able to read the user’s name.

Configuring Skill Permissions

We’re going to leverage some of this user information in the Star Port 75 Travel skill to greet a user by name when they launch the skill. Therefore, we need to configure our skill to request permission for the user’s given names. To do that, we’ll add a new permissions property in the skill’s manifest (skill.json) that looks like this:

 { "manifest": { ...
  "permissions": [ { "name": "alexa::profile:given_name:read" } ] } }

Here we’re configuring the skill for one permission. The alexa::profile:given_name:read entry is for permission to read the user’s given name. Notice that the permissions property is an array. Therefore, if your skill needs additional permissions, you can configure more by listing them alongside the alexa::profile:given_name:read permission under permissions. A few other permissions you might ask for include the following:

  • Full name—alexa::profile:name:read
  • Email address—alexa::profile:email:read
  • Phone/mobile number—alexa::profile:mobile_number:read
  • Device address—alexa::devices:all:address:full:read

For example, our skill only needs the user’s given name. Suppose, however, that your skill needs the user’s email address and their phone number in addition to their first name. You could then configure the permissions property like this:

 {
 "manifest"​: {
 ...
 "permissions"​: [
  { ​"name"​: ​"alexa::profile:given_name:read"​ },
  { ​"name"​: ​"alexa::profile:email:read"​ },
  { ​"name"​: ​"alexa::profile:mobile_number:read"​ }
  ]
  }
 }

Although the skill may be configured with one or more permissions, Alexa won’t automatically request those permissions when the skill is launched. The configuration of the permissions property in the manifest only specifies the permissions that a skill may request, not that it necessarily will. We’ll soon see how to have the skill request permission. But first, let’s assume that permission has been given and fetch the user’s given name.

Requesting User Data

When requesting a user’s personal information from Alexa, you’ll need three things:

  • The Alexa API base URL
  • A path to the endpoint for the specific data you want
  • An authorized API access token

As it turns out, two out of those three things are readily available to you in the handlerInput object passed into the request handler. From the handlerInput, you can use code like the following to obtain the base URL and the API access token:

 const apiBaseUrl = handlerInput.context.System.apiEndpoint;
 const accessToken = handlerInput.context.System.apiAccessToken;

The API base URL may vary depending on the user’s location, so Alexa gives the correct one to you via the handlerInput so that you don’t have to write code in your skill to figure it out. By appending the appropriate path to the API’s base URL, you can use any HTTP client library you like to request any personal information that your skill has permission to read. These are a few API paths you might find useful:

  • Full name—/v2/accounts/~current/settings/Profile.name
  • Given name—/v2/accounts/~current/settings/Profile.givenName
  • Email address—/v2/accounts/~current/settings/Profile.email
  • Phone/mobile number—/v2/accounts/~current/settings/Profile.mobileNumber
  • Device address—/v1/devices/{deviceId}/settings/address
  • Country and postal code—/v1/devices/{deviceId}/settings/address/countryAndPostalCode

For example, if you want to fetch the user’s given name, you can create a full URL for the request like this:

 const​ givenNameUrl = apiBaseUrl +
 '/v2/accounts/~current/settings/Profile.givenName'​;

That will work as long as you also pass the API access token in the request and as long as the token has been granted permission to access the user’s given name. It’s an OAuth2 Bearer token—more specifically, a JSON Web Token (JWT)[18]—so you’ll need to pass it in the Authorization header.

Putting it all together and using the node-fetch module,[19] you can fetch the user’s given name with the following:

 const​ apiBaseUrl = handlerInput.context.System.apiEndpoint;
 const​ accessToken = handlerInput.context.System.apiAccessToken;
 const​ givenNameUrl = apiBaseUrl +
 '/v2/accounts/~current/settings/Profile.givenName'​;
 
 const​ givenName = ​await​ fetch(
  givenNameUrl,
  {
  method: ​'GET'​,
  headers: {
 'Accept'​: ​'application/json'​,
 'Authorization'​: ​'Bearer '​ + accessToken
  }
  }
 );

If all of that seems daunting, then you may be delighted to know that ASK has made it much easier for you by providing a ready-to-use client for accessing user information. Instead of extracting the base URL and token from handlerInput, concatenating a path to the base URL, and using them with an HTTP library directly, you can use the UpsServiceClient which gives you easy access to user information more conveniently. The simplified code looks like this:

 const​ upsClient = handlerInput
  .serviceClientFactory.getUpsServiceClient();
 const​ givenName = ​await​ upsClient.getProfileGivenName();

That’s a lot easier! The serviceClientFactory property of handlerInput gives access to a handful of service clients that you might use in your skill’s handler code. Among them is the UpsServiceClient, which provides (among other things) the user’s given name via the getProfileGivenName() function. Notice that we need to apply the await keyword to the call to getProfileGivenName(). This is because getProfileGivenName() is performed asynchronously via a Javascript promise, but the handler needs to wait on the result of that promise before moving on.

You may be wondering why it’s named UpsServiceClient. As it turns out, UpsServiceClient is a service client that aggregates access to two Alexa services in a single client, the Customer Preference Service and the Customer Settings Service. Since this client unifies those two services, Amazon named it the Unified Preference Service, or UPS as an acronym, thus the name UpsServiceClient for the client object.

In addition to the user’s given name, the UpsServiceClient offers a half-dozen other bits of information that you might find useful:

  • Full name—getProfileName()
  • Email address—getProfileEmail()
  • Phone/Mobile number—getProfileMobileNumber()
  • Preferred unit of distance—getSystemDistanceUnits(deviceId)
  • Preferred unit of temperature—getSystemTemperatureUnit(deviceId)
  • Time zone—getSystemTimeZone(deviceId)

Notice that the last three items are device specific and require that the device ID be passed in as an argument. The device ID can be obtained from the handlerInput via the context.System.device.deviceId property.

In order to use the service client factory, we’ll need to configure an API client in the skill builder. By default, the serviceClientFactory will be null and of no use in fetching a user’s given name (or any other information for that matter). By configuring an API client in the skill builder, we’ll have no trouble at all using serviceClientFactory to work with the unified preference service. The following snippet from index.js shows how to configure the default API client:

 exports.handler = Alexa.SkillBuilders.custom()
  .addRequestHandlers(
  ...
  )
  .addErrorHandlers(
  StandardHandlers.ErrorHandler)
» .withApiClient(​new​ Alexa.DefaultApiClient())
  .addRequestInterceptors(
  LocalisationRequestInterceptor)
  .lambda();

Now let’s apply what we’ve learned about the service client in the context of the launch request handler’s handle() function:

 const​ LaunchRequestHandler = {
  canHandle(handlerInput) {
 const​ reqEnvelope = handlerInput.requestEnvelope;
 return​ Alexa.getRequestType(reqEnvelope) === ​'LaunchRequest'​;
  },
 async​ handle(handlerInput) {
 const​ { serviceClientFactory, responseBuilder } = handlerInput;
 
 let​ speakOutput = handlerInput.t(​'WELCOME_MSG'​);
 
 try​ {
 const​ upsClient = serviceClientFactory.getUpsServiceClient();
 const​ givenName = ​await​ upsClient.getProfileGivenName();
  speakOutput = handlerInput.t(​'WELCOME_MSG_PERSONAL'​,
  {
  givenName: givenName
  });
  } ​catch​ (error) {
  }
 
 return​ responseBuilder
  .speak(speakOutput)
  .reprompt(speakOutput)
  .getResponse();
  }
 };

This first thing you’ll notice about this new implementation of handle() is that it extracts the responseBuilder and serviceClientFactory objects out of handlerInput to make them more conveniently available later in the function.

Next, the speakOutput variable is set to the default welcome message before attempting to fetch the user’s given name. Assuming that we are able to obtain the user’s given name, we pass it in an object to the t() function to be used in a more personal welcome message. The more personal “WELCOME_MSG_PERSONAL” message is defined in languageStrings.js as follows:

 module.exports = {
  en: {
  translation: {
  ...
  WELCOME_MSG_PERSONAL: ​'Welcome back to Star Port 75 Travel, '​ +
 '{{givenName}}! How can I help you?'​,
  ...
  }
  }
 }

The handle() function ends by returning the personal greeting in the response.

If an error is thrown while fetching the user’s given name—most likely because the skill doesn’t have permission—then the catch block does nothing, the generic greeting remains unchanged, and the handler returns the less personal greeting.

Ultimately, it’s completely up to the user to decide whether or not to grant the skill permission to access their personal information. The skill shouldn’t pester them too much about it unless the information is critical to the function of the skill. On the other hand, even if the desired information isn’t critical, you’ll want to offer some way to let the user know that your skill wants such permission. Let’s see how to prompt the user for permission without being annoying about it.

Asking for Permission

In order for a user to grant your skill permission to access their personal information, they’ll need to find your skill in the companion application on their mobile device or in the browser-based companion application.[20] However, because permission is granted out-of-band in a separate device, the user may not be aware that they should go to the companion application to give permission. Therefore, it might be a good idea to prompt them for permission.

Depending on how important the permission is to your skill’s functionality, there are four approaches you can take:

  • Not prompt the user at all. Let them discover the permissions on their own if and when they explore your skill in the companion application. (Not recommended)

  • Passively prompt the user to grant permission in the companion application. They will see that your skill is asking for permission if they open the companion application and can decide whether or not to grant it.

  • Actively prompt the user to grant permission in the companion application. This involves having Alexa explicitly ask the user to grant the skill permission via the companion application.

  • Demand that permission be granted with a permissions consent card. The skill will not function fully until the user has granted permission in the companion application. (Only recommended if the skill cannot function without the required information.)

At this point, the Star Port 75 Travel skill is not prompting the user for permission in any way. That means, unless they open the companion application, navigate to the skill’s configuration, and happen to notice that it is requesting permission to read their given name, they will probably never grant the skill permission and it will always greet them with the generic, impersonal greeting. Therefore, we should consider applying one of the other three approaches to gain permission from the user.

Regardless of which approach we choose, the technique is essentially the same. That is, along with the text we have Alexa speak in the response, we should include a permissions consent card in our response. Doing so involves calling the withAskForPermissionsConsentCard() method on the responseBuilder. For example, if we are explicitly asking the user to grant permission for reading the user’s given name, then the handler method might build a response like this:

 return​ responseBuilder
  .speak(​"Please open the Amazon Alexa app and grant me permission "
  + ​" to access your email address "​)
  .withAskForPermissionsConsentCard(
  [
 'alexa::profile:given_name:read'
  ])
  .getResponse();

A card is a simple visual representation of a skill response that is displayed in the Alexa companion application on the user’s mobile device or in the browser-based companion application website. We’ll look at cards in more detail in Chapter 9, Complementing Responses with Cards. But for now it’s enough to know a permissions consent card is a special card that prompts the user to grant one or more permissions to a skill. When displayed in the iOS companion application, the card will look something like this:

images/integration/permissions-given-name.png

To grant permission, the user will click on the “MANAGE” button which should launch the Skill Permissions screen shown here:

images/integration/permissions-settings.png

 

From the skill permissions screen, they can check one or all permissions that they want to grant and click the “SAVE PERMISSIONS” button to submit their changes. Once permission is granted, the skill will be able to read the given name (or whatever information permission was granted for).

A permissions consent card can be returned in response to any request. It is usually best to avoid explicitly asking for permission from a user unless they’re trying to use some functionality of the skill that requires that permission.

In the Star Port 75 Travel skill, permission to read the user’s given name isn’t strictly required. While we’d like to greet the user in a personal way, not being able to will in no way limit the ability of the skill to plan a trip through the solar system. Therefore, it’s probably best if we send a permissions consent card in response to the launch request, but not have Alexa vocally request the permission. When the user opens their companion application, they may notice the card and grant the requested permission.

To passively request permission to read the user’s given name, we’ll add a few lines in the catch block of the launch request handler that calls withAskForPermissionsConsentCard():

 try​ {
 const​ upsClient = serviceClientFactory.getUpsServiceClient();
 const​ givenName = ​await​ upsClient.getProfileGivenName();
  speakOutput = handlerInput.t(​'WELCOME_MSG_PERSONAL'​,
  {
  givenName: givenName
  });
 } ​catch​ (error) {
»if​ (error.name === ​'ServiceError'​ && error.statusCode == 403) {
» responseBuilder
» .withAskForPermissionsConsentCard([
»'alexa::profile:given_name:read'​])
» } ​else​ {
» console.error(​"Error reading the given name: "​, error);
» }
 }

If the user has already granted permission, then the try block will have no problem reading the given name and setting the greeting message to a personal greeting. But if it fails and if the error is a service error indicating that the skill doesn’t have permission to read the given name (for example, an HTTP 403 status code), then it will slip a permission consent card into the request. Any other error is simply logged—there’s no reason to stop the skill or alert the user if it can’t read the given name.

Trying It Out

We’re now ready to kick the tires on this new personal greeting and permission consent card. First, deploy the skill. Then launch the skill on your device or in the developer console without granting permission to read the given name. For example using the ask dialog client, the interaction looks like this:

 $ ​​ask​​ ​​dialog​​ ​​--locale​​ ​​en-US
  User > open star port seventy five
  Alexa > Welcome to Star Port 75 Travel. How can I help you?

As you can see, because the skill doesn’t have permission to fetch the user’s first name, it produces the generic greeting.

So far, so good. Now open the companion application on your mobile device or visit the companion application website in your web browser to see the permission consent card. Using the companion application, grant the requested permission and try again:

 $ ​​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?

As you can see, the results are quite different. This time, it knows the user’s first name and is able to produce a personal greeting.

We could stop here knowing that everything works as expected. But manual testing with ask dialog or an actual device isn’t repeatable and prone to human error. Let’s see how to write a proper automated test using BST.

Mocking and Testing Alexa Services

Under the covers, the getProfileGivenName() method makes an HTTP request to a REST endpoint provided by Amazon. Therefore, if we’re going to test our new friendlier launch request handler, we’re going to need a way to mock the request to that endpoint, so that it doesn’t try to send a request to the actual endpoint. To do that, we’re going to mix a network mocking library called Nock[21] with a feature of bst for filtering requests.

To start, we’ll need to install Nock using npm install:

 $ ​​npm​​ ​​install​​ ​​--prefix=lambda​​ ​​--save-dev​​ ​​nock

We’ll use Nock in a BST filter to mock the behavior of the given name endpoint. A BST filter[22] is a Javascript module that implements one or more functions that get a chance to review or even change a request sent by a test before it is handed off to the request handler. A filter can intercept test flow for the entire test suite, for a single test, for each request or response, or when the test or test suite ends. To test our new friendlier launch request handler, we’re going to write a filter that intercepts requests before they make it to the request handler and uses Nock to mock the given name endpoint.

The filter will only be needed for testing, so we should create it in the test/unit folder and not the lambda folder, so that it won’t be packed up and deployed with the rest of our skill code when we deploy the skill. The filter will be in a Javascript module named mock-given-name-filter.js that looks like this:

 const​ nock = require(​'nock'​);
 
 module.exports = {
  onRequest: (test, requestEnvelope) => {
 const​ mockGivenName = test.testSuite.configuration.givenName;
 const​ TEST_API_ENDPOINT = ​'https://api.amazonalexa.com'​;
  requestEnvelope.context.System.apiEndpoint = TEST_API_ENDPOINT;
  requestEnvelope.context.System.apiAccessToken = ​'API_ACCESS_TOKEN'​;
 if​ (mockGivenName) {
  nock(TEST_API_ENDPOINT)
  .​get​(​'/v2/accounts/~current/settings/Profile.givenName'​)
  .reply(200, ​`"​${mockGivenName}​"`​);
  } ​else​ {
  nock(TEST_API_ENDPOINT)
  .​get​(​'/v2/accounts/~current/settings/Profile.givenName'​)
  .reply(403,
  {
 "code"​:​"ACCESS_DENIED"​,
 "message"​:​"Access denied with reason: FORBIDDEN"
  });
  }
  }
 };

The module declares a single filter method, onRequest(), which will be invoked before every request handled by a skill’s handler method when running tests with bst test. It is given a test object that includes details about the test that triggered the request, and a requestEnvelope object that is the request envelope object. We can use these objects in the filter to make decisions and even set or change details of the request before the request is sent to the handler.

The first thing this filter does is extract the value of a givenName property from the test suite’s configuration. This is a custom test configuration property that we’ll set in our test suite in a moment. The filter checks to see if it is set and, if so, will continue to setup the mock service. If givenName is not set, then it returns an error in the response with an HTTP 403 status as an indication that the skill doesn’t have permission to make the request.

In a deployed skill, the base API URL is provided by the platform in the requestEnvelope as context.System.apiEndpoint. When running with bst test, however, the API endpoint is left undefined, so we’ll need to set it in the request so that getProfileGivenName() will be able to form the request. Since we’ll be mocking the service, the base URL we use in the test isn’t important. Nevertheless, we’ll set it to “https://api.amazonalexa.com”—the base URL used for skills in the North American region.

Similarly, we’ll also need to provide the access token in the request by setting a value on requestEnvelope.context.System.apiAccessToken. Again, the actual value given here is unimportant, but it must be set or else a call to getProfileGivenName() won’t even attempt to send a request to the service. In a deployed skill the token is a JWT, but for testing purposes, we’ll just set it to “API_ACCESS_TOKEN”.

Finally, the filter mocks the endpoint, specifying that if a GET request for “/v2/accounts/~current/settings/Profile.givenName” is received, then it should reply with an HTTP status of 200 and a JSON payload consisting of a simple string, which is the name specified by the givenName property from the test suite.

Now let’s use the filter to test the launch request. As you’ll recall from chapter Chapter 2, Testing Alexa Skills, we already have a test for the launch request handler that asserts the proper output speech when there is no permission to fetch the given name. Here’s an updated version of it that applies the filter to assert that a permissions consent card is sent in the response:

 ---
 configuration:
  locales: ​en-US
  filter: ​mock-given-name-filter.js
 
 ---
 - test: ​Launch request
 - LaunchRequest:
  - prompt: ​Welcome to Star Port 75 Travel.
 How can I help you?
  - ​response.card.type​: ​AskForPermissionsConsent

In the test suite’s configuration, we set the filter property to reference the filter module we created. But since we don’t set a givenName property, the filter will reply to the service request with an HTTP 403 response. Therefore, the skill response should have the default greeting along with a permissions consent card, asserted by verifying that response.card.type is “AskForPermissionsConsent”.

The more interesting test case is the one where the request for the user’s given name actually returns a value. To test that case, we can make a duplicate of the original test, tweaking it to specify the filter, the given name, and to assert that the personal greeting is returned. Here’s what that test case will look like:

 ---
 configuration:
  locales: ​en-US
  filter: ​mock-given-name-filter.js
  givenName: ​Craig
 
 ---
 - test: ​Launch request
 - LaunchRequest:
  - prompt: ​Welcome back to Star Port 75 Travel, Craig! How can I help you?

In the test suite’s configuration, we set the filter property to reference the filter module we created. And we set the givenName property with “Craig”, a name we will expect to see in the personal greeting returned from the launch request. Finally, the test for the launch request asserts that the output speech contains the personal message with the expected name.

Let’s run both test suites, standard-handlers-authorized.test.yml and standard-handlers-unauthorized.test.yml, and see how they fare:

 $ ​​bst​​ ​​test​​ ​​--jest.collectCoverage=false​​ ​​standard-handlers
 
 BST: v2.6.0 Node: v17.6.0
 
 PASS test/standard-handlers-unauthorized.test.yml
  en-US
  Launch request
  ✓ LaunchRequest
 
 PASS test/standard-handlers-authorized.test.yml
  en-US
  Launch request
  ✓ LaunchRequest
 
 Test Suites: 2 passed, 2 total
 Tests: 2 passed, 2 total
 Snapshots: 0 total
 Time: 1.077s, estimated 2s
 Ran all test suites.

It looks like everything passed! Now we have a repeatable test to ensure that our launch request handler will always send a personal greeting to users who have granted it permission to read their given name. And, if they haven’t given permission, we have a test that will passively request permission with a permissions consent card.

Although Alexa offers some useful information via the unified preference service, it’s not the only service you can use in your skills. We’ll see a few more of Alexa’s services in later chapters. But for now, let’s think outside of the skill and see how to work external services using a technique called account linking.

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

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