diff options
-rw-r--r-- | htdocs/amazon.js | 323 | ||||
-rw-r--r-- | htdocs/binding.css | 2 | ||||
-rw-r--r-- | htdocs/binding.js | 24 | ||||
-rw-r--r-- | htdocs/index.html | 20 |
4 files changed, 367 insertions, 2 deletions
diff --git a/htdocs/amazon.js b/htdocs/amazon.js new file mode 100644 index 0000000..05704f1 --- /dev/null +++ b/htdocs/amazon.js @@ -0,0 +1,323 @@ +/* + * Copyright 2018-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0/ + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ +AMAZON = function() { + +var afb; +var alexaWs; + +var base = { + base: "api", + token: "HELLO", +}; + +// GUID generator for generating device serial number. +function guid() { + function s4() { + return Math.floor((1 + Math.random()) * 0x10000) + .toString(16) + .substring(1); + } + return s4() + s4() + '-' + s4() + '-' + s4() + '-' + s4() + '-' + s4() + s4() + s4(); +} + +/*********************************************/ +/**** ****/ +/**** AMAZON_cbl ****/ +/**** ****/ +/*********************************************/ +var AMAZON_Cbl; +{ + const amazonHostUrl = "https://api.amazon.com"; + const amazonCodePairUrl = amazonHostUrl + "/auth/O2/create/codepair"; + const amazonTokenUrl = amazonHostUrl + "/auth/O2/token"; + const deviceSerialNumber = guid(); + var clientID = localStorage.getItem("client_id"); + var productID = localStorage.getItem("product_id"); + var alexaVAAddress = localStorage.getItem("alexa_va_address"); + var alexaVAConnected = false; + var alexaVAAddressInput; + var clientIDInput; + var productIDInput; + + AMAZON_Cbl = function() { + // Alexa VA Address + const alexaVAAddressInput = document.getElementById('alexa-va-address'); + alexaVAAddress = alexaVAAddressInput.value; + connectToAlexaVA(alexaVAAddress); + + alexaVAAddressInput.addEventListener("change",(evt) => { + var newAlexaVAAddress = alexaVAAddressInput.value; + if (alexaVAAddress != newAlexaVAAddress) { + connectToAlexaVA(newAlexaVAAddress); + localStorage.setItem("alexa_va_address", newAlexaVAAddress); + } + }); + + // Client ID + const clientIDInput = document.getElementById('client-id'); + clientIDInput.addEventListener("change",(evt) => { + var newClientID = clientIDInput.value; + if (clientID != newClientID) { + clientID = newClientID; + localStorage.setItem("client_id", newClientID); + } + }); + + // Product ID + const productIDInput = document.getElementById('product-id'); + productIDInput.addEventListener("change",(evt) => { + var newProductID = productIDInput.value; + if (productID != newProductID) { + productID = newProductID; + localStorage.setItem("product_id", newProductID); + } + }); + } + + function connectToAlexaVA(address) { + base.host = address; + afb = new AFB(base, "secret"); + + function onopen() { + console.log("Connected to Alexa VA"); + alexaVAConnected = true; + } + + function onabort() { + console.log("Alexa VA connection aborted."); + alexaVAConnected = false; + } + + alexaWs = new afb.ws(onopen, onabort); + } + + function sendRequest(httpReq, paramsJson, url, responseCb) { + httpReq.onreadystatechange = responseCb; + var paramsQueryString = Object.keys(paramsJson).map(key => key + '=' + paramsJson[key]).join('&'); + httpReq.open("POST", url, true); + httpReq.setRequestHeader("Content-type", "application/x-www-form-urlencoded"); + httpReq.send(paramsQueryString); + } + + //********************************************** + // Generic function to call VA binder + //*********************************************** + function callVABinder(voiceAgent, verb, query) { + console.log(voiceAgent.api, verb, query); + + // ws.call return a Promise + return alexaWs.call(voiceAgent.api + '/' + verb, query) + .then(function (res) { + log.reply(res); + count++; + return res; + }) + .catch(function (err) { + log.reply(err); + count++; + throw err; + }); + }; + + function updateAccessToken(voiceAgent, tokenResponseJson) { + if (alexaVAAddress === undefined || alexaVAAddress === null) { + console.log("No Alexa VA. So not updating the access token."); + return; + } + + // store the access and refresh tokens. + if (typeof(Storage) !== "undefined") { + localStorage.setItem("access_token", tokenResponseJson["access_token"]); + localStorage.setItem("refresh_token", tokenResponseJson["refresh_token"]); + } + + // Set the auth token + if (alexaVAConnected) { + // Set new token + const query = {"token": tokenResponseJson["access_token"]}; + callVABinder(voiceAgent, 'setAuthToken', query); + } + + // Refresh the token as soon as it expires. + setTimeout(refreshToken, tokenResponseJson["expires_in"] * 1000); + } + + function refreshToken(voiceAgent) { + if (voiceAgent == "undefined") { + console.log("Error: VoiceAgent undefined"); + return; + } + + var refreshToken = localStorage.getItem("refresh_token"); + if (refreshToken == null) { + console.log("Error: No refresh token"); + return; + } + + var paramsJson = { + "grant_type":"refresh_token", + "refresh_token":refreshToken, + "client_id":clientID, + }; + + const tokenRefreshReq = new XMLHttpRequest(); + sendRequest(tokenRefreshReq, paramsJson, amazonTokenUrl, function() { + if (tokenRefreshReq.readyState == 4) { + if (tokenRefreshReq.status == 200) { + console.log("Got access token " + tokenRefreshReq.responseText); + var tokenResponseJson = JSON.parse(tokenRefreshReq.responseText); + updateAccessToken(voiceAgent, tokenResponseJson); + } else { + console.log("Failed to refresh access token: " + tokenRefreshReq.responseText); + } + } + }); + } + + function displayUserCodeAndURI(authResponseJson) { + const modal = document.getElementById('login-with-amazon'); + const cblStatusDiv = document.createElement("div"); + const cblStatusMsg = document.createElement("p"); + const blank = "_blank"; + + var cblPage = authResponseJson["verification_uri"] + "?cbl-code=" + authResponseJson["user_code"] + var msg = "To use Alexa,you must sign in to Amazon.<br> Go to " + + "<a href=" + cblPage + " target="+ blank+ " >" + + cblPage + "</a>"; + cblStatusMsg.innerHTML = msg; + cblStatusDiv.appendChild(cblStatusMsg); + modal.appendChild(cblStatusDiv); + + const closeBtn = document.createElement("button"); + closeBtn.addEventListener('click', (evt) => { + modal.close(); + }); + closeBtn.style = "margin: 10px"; + closeBtn.innerHTML = "Close"; + modal.appendChild(closeBtn); + } + + function hideLoginUI() { + const loginDiv = document.getElementById('login-area'); + loginDiv.style.display = "none"; + } + + function login(voiceAgent) { + if (voiceAgent == undefined) { + console.log("Error: VoiceAgent undefined"); + return; + } + + const modal = document.getElementById('login-with-amazon'); + const submitBtn = document.getElementById('submit-btn'); + const cancelBtn = document.getElementById('cancel-btn'); + submitBtn.addEventListener('click', (evt) => { + console.log("Alexa Destination address set to: " + alexaVAAddress); + startLoginProcess(voiceAgent); + }); + + cancelBtn.addEventListener('click', (evt) => { + modal.close(); + }); + + const alexaVAAddressInput = document.getElementById('alexa-va-address'); + alexaVAAddressInput.value = alexaVAAddress; + + const clientIDInput = document.getElementById('client-id'); + clientIDInput.value = clientID; + + const productIDInput = document.getElementById('product-id'); + productIDInput.value = productID; + + modal.showModal(); + } + + function startLoginProcess(voiceAgent) { + if (clientID == null || productID == null || alexaVAAddress == null) { + console.log("Required information missing to start login process."); + return; + } + + var reqJson = { + "response_type": "device_code", + "client_id": clientID, + "scope":"alexa:all", + "scope_data": JSON.stringify({ + "alexa:all": { + "productID":productID, + "productInstanceAttributes" : { + "deviceSerialNumber": deviceSerialNumber + } + } + }) + }; + + const authReq = new XMLHttpRequest(); + var tokenUrl = amazonTokenUrl; + sendRequest(authReq, reqJson, amazonCodePairUrl, function() { + if (authReq.readyState == 4) { + if (authReq.status == 200) { + var authResponse = JSON.parse(authReq.responseText); + console.log("Got auth codepair " + authReq.responseText); + hideLoginUI(); + displayUserCodeAndURI(authResponse); + var maxTokenReqCnt = authResponse["expires_in"] / authResponse["interval"]; + var tokenReqFuncId = setTimeout(function tokenReqFunc() { + var reqJson = { + "grant_type":"device_code", + "device_code":authResponse["device_code"], + "user_code":authResponse["user_code"] + }; + const tokenReq = new XMLHttpRequest(); + sendRequest(tokenReq, reqJson, tokenUrl, function() { + if (tokenReq.readyState == 4) { + if (tokenReq.status == 200) { + console.log("Got access token " + tokenReq.responseText); + var tokenResponseJson = JSON.parse(tokenReq.responseText); + updateAccessToken(voiceAgent, tokenResponseJson); + } + else { + maxTokenReqCnt--; + console.log("Retrying... " + tokenReq.responseText); + setTimeout(tokenReqFunc, authResponse["interval"] * 1000); + } + } + }); + }, authResponse["interval"] * 1000); + // Cancel if max token request attempts are reached. + if (maxTokenReqCnt == 0) { + console.log("Reached max token request attemps limit."); + } + } else { + console.log(authReq.status); + } + } + }); + } + + AMAZON_Cbl.prototype = { + login: login, + refreshToken: refreshToken, + }; +} +/*********************************************/ +/**** ****/ +/**** ****/ +/**** ****/ +/*********************************************/ +return { + cbl: AMAZON_Cbl +}; +};
\ No newline at end of file diff --git a/htdocs/binding.css b/htdocs/binding.css index 99f84b4..9ee1303 100644 --- a/htdocs/binding.css +++ b/htdocs/binding.css @@ -88,7 +88,7 @@ dialog::backdrop { } h3.dialogheader { - background-color: rgb(177, 177, 236); + background-color: lightgreen; padding: 1ch; } diff --git a/htdocs/binding.js b/htdocs/binding.js index c24d62e..7e7439b 100644 --- a/htdocs/binding.js +++ b/htdocs/binding.js @@ -3,6 +3,8 @@ var ws; var evtIdx = 0; var count = 0; +var amazon = new AMAZON(); +var amazonCbl; //********************************************** // Logger @@ -10,7 +12,7 @@ var count = 0; var log = { command: function (api, verb, query) { console.log("subscribe api=" + api + " verb=" + verb + " query=", query); - var question = urlWS + "/" + api + "/" + verb + "?query=" + JSON.stringify(query); + var question = afb.url + "/" + api + "/" + verb + "?query=" + JSON.stringify(query); log._write("question", count + ": " + log.syntaxHighlight(question)); }, @@ -92,6 +94,8 @@ function init(elemID, api, verb, query) { document.getElementById("connected").innerHTML = "Binder WS Active"; document.getElementById("connected").style.background = "lightgreen"; ws.onevent("*", log.event); + // Fetch and render voice agents. + fetchAndRenderVoiceAgents(); } function onabort() { @@ -165,6 +169,24 @@ function addVoiceAgent(containerDiv, voiceAgent, isDefault) { subscribeBtn.innerHTML = 'Subscribe'; agentDiv.appendChild(subscribeBtn); + // Login implementation for Alexa Voice Agent + if (voiceAgent.name == "Alexa") { + amazonCbl = new amazon.cbl(); + if (typeof(Storage) !== "undefined" && + localStorage.getItem("access_token") !== null && + localStorage.getItem("refresh_token") !== null) { + amazonCbl.refreshToken(voiceAgent); + } else { + const loginWithAmazonBtn = document.createElement("button"); + loginWithAmazonBtn.addEventListener('click', (evt) => { + loginWithAmazonBtn.style.visibility = "hidden"; + amazonCbl.login(voiceAgent); + }); + loginWithAmazonBtn.innerHTML = 'Login With Amazon!!'; + agentDiv.appendChild(loginWithAmazonBtn); + } + } + containerDiv.appendChild(agentDiv); } diff --git a/htdocs/index.html b/htdocs/index.html index 0480c35..bf5f840 100644 --- a/htdocs/index.html +++ b/htdocs/index.html @@ -4,6 +4,7 @@ <title>VSHL API Test</title> <link rel="stylesheet" href="binding.css"> <script type="text/javascript" src="AFB.js"></script> + <script type="text/javascript" src="amazon.js"></script> <script type="text/javascript" src="binding.js"></script> </head> @@ -69,6 +70,25 @@ </footer> </dialog> + <dialog id="login-with-amazon"> + <h3 class="dialogheader">Login with Amazon !!</h3> + <div id="login-area"> + <div> + Alexa VA URL: <input type="text" id="alexa-va-address" value="localhost:1111"> <br><br> + Client ID : <input type="text" id="client-id"> <br><br> + Product ID : <input type="text" id="product-id"> <br><br> + To generate client and product ID, please register a new AVS product for + <i><b>Other devices and platforms</b></i> using instructions in + <a href="https://developer.amazon.com/docs/alexa-voice-service/register-a-product.html" target="_blank">this </a> + link.<br><br> + </div> + <footer id ="login-with-amazon-footer"> + <button id="submit-btn" type="button" style="margin: 10px">Login</button> + <button id="cancel-btn" type="button" style="margin: 10px">Cancel</button> + </footer> + </div> + </dialog> + <div id="top" class="row"> <div id='actions' class="col1"> <div> |