From d5120a1ba237951ba843edc3cc7bb84b3876f1a4 Mon Sep 17 00:00:00 2001 From: GotPPay Date: Sat, 2 Dec 2017 22:48:45 +0100 Subject: [PATCH 01/83] functional application without amazon update --- backend/express.js | 43 ++++--- backend/helpers/amazon.js | 7 ++ web/package.json | 6 +- web/src/App.js | 174 +++++++++++++++++++--------- web/src/components/IntentDetails.js | 48 +++++++- web/src/components/IntentItem.js | 17 +-- web/src/components/IntentList.js | 4 +- web/src/components/LaunchRequest.js | 64 ++++++++++ web/src/config.js | 12 +- web/src/css/Intent.css | 37 +++++- web/src/css/Intent.scss | 40 ++++++- web/src/lib/api.js | 21 ++-- 12 files changed, 375 insertions(+), 98 deletions(-) create mode 100644 backend/helpers/amazon.js create mode 100644 web/src/components/LaunchRequest.js diff --git a/backend/express.js b/backend/express.js index 252e564..e40491a 100644 --- a/backend/express.js +++ b/backend/express.js @@ -1,3 +1,5 @@ +import { sendUpdateToAmazon } from './helpers/amazon'; + var express = require('express'); var alexa = require('alexa-app'); @@ -12,13 +14,22 @@ var ObjectID = require ('mongodb').ObjectID; const router = express.Router (); -router.get ('/intents', async (req, res, next) => { +router.get ('/getSkill/:id', async (req, res, next) => { try { const id = req.params.id; - db.collection ('intent_list').find({}).toArray((err,result)=>{ - if (err) throw err; - res.json(result); + if (id.length !== 24){ + res.json([]); + throw("error"); + } + + db.collection ('skill_list').find({_id: ObjectID(id)}).toArray((err,result)=>{ + if (err){ + throw err; + res.json([]); + }else{ + res.json(result); + } }); } catch (e) { @@ -27,10 +38,11 @@ router.get ('/intents', async (req, res, next) => { } }); -router.get ('/deleteIntent/:id', async (req, res, next) => { + +router.get ('/deleteSkill/:skillID', async (req, res, next) => { try { let id = req.params.id; - let result = db.collection('intent_list').remove({_id: ObjectID(id)},(err,result)=>{ + let result = db.collection('skill_list').remove({_id: ObjectID(id)},(err,result)=>{ if (err) throw err; res.json(result); }); @@ -40,15 +52,20 @@ router.get ('/deleteIntent/:id', async (req, res, next) => { } }); -router.post ('/updateIntent/:id', async (req, res, next) => { +router.post ('/updateSkill/:id', async (req, res, next) => { try { let id = req.params.id; - let intent = req.body; - delete intent._id; - let result = db.collection('intent_list').update({_id: ObjectID(id)}, intent,{upsert:true}, (err, result)=>{ - if (err) throw(err); - res.json(result); - }); + let skill = req.body; + delete skill._id; + if (id !== '-1'){ + let result = db.collection('skill_list').update({_id: ObjectID(id)}, skill,{upsert:true}, (err, result)=>{ + if (err) throw(err); + res.json(result); + }); + }else{ + //no new skills for now + } + } catch (e) { console.log ('error:', e); next (e); diff --git a/backend/helpers/amazon.js b/backend/helpers/amazon.js new file mode 100644 index 0000000..7789579 --- /dev/null +++ b/backend/helpers/amazon.js @@ -0,0 +1,7 @@ + + + +export const sendUpdateToAmazon = (id)=>{ + //id - skill ID in database + +} \ No newline at end of file diff --git a/web/package.json b/web/package.json index fe7f721..0c49f3b 100644 --- a/web/package.json +++ b/web/package.json @@ -6,7 +6,8 @@ "react": "^16.2.0", "react-dom": "^16.2.0", "react-md": "^1.2.8", - "react-scripts": "1.0.17" + "react-scripts": "1.0.17", + "webfontloader": "^1.6.28" }, "scripts": { "build-css": "node-sass-chokidar --include-path ./node_modules src/ -o src/", @@ -20,6 +21,7 @@ }, "devDependencies": { "node-sass": "^4.7.2", - "nodemon": "^1.12.1" + "nodemon": "^1.12.1", + "npm-run-all": "^4.1.2" } } diff --git a/web/src/App.js b/web/src/App.js index 34e7b2b..7761b92 100644 --- a/web/src/App.js +++ b/web/src/App.js @@ -2,86 +2,152 @@ import React, { Component } from 'react'; import './css/App.css'; import IntentList from './components/IntentList'; import IntentDetails from './components/IntentDetails'; +import LaunchRequest from './components/LaunchRequest'; -import {getAllIntents, deleteIntent, updateIntent} from './lib/api' +import {getSkill, updateSkill} from './lib/api' class App extends Component { constructor(props){ super(props); - this.state={allIntents:[], selectedIntent: {questions:[''],answer:''}, selectedIndex:-1}; + this.state={_id:'5a22ffd36ce046c749739453', + skillID:'', + invocationName:'Saburly', + invocationAnswer:'We are saburly', + allIntents:[], + selectedIntent: {intentName:'',questions:[''],answer:''}, + selectedIndex:-1, + launchRequest:false}; - getAllIntents().then(l=> l.text()).then(result=>{ - this.setState({allIntents: JSON.parse(result), selectedIntent: this.state.selectedIntent}) + getSkill(this.state._id).then(l=> l.text()).then(result=>{ + let jResult = JSON.parse(result)[0]; + if (jResult===undefined) return; + this.setState({ skillID:jResult.skillID, invocationName: jResult.invocationName, + invocationAnswer: jResult.invocationAnswer, + allIntents: jResult.intents}) }) this.handleIntentClick = this.handleIntentClick.bind(this); + this.handleLaunchRequestClick = this.handleLaunchRequestClick.bind(this); this.handleDeleteIntentClick = this.handleDeleteIntentClick.bind(this); this.handleSaveIntentClick = this.handleSaveIntentClick.bind(this); this.handleAddIntentClick = this.handleAddIntentClick.bind(this); + this.handleSaveLaunchRequestClick = this.handleSaveLaunchRequestClick.bind(this); + this.createSkill = this.createSkill.bind(this); } render() { - return ( -
-
-

Tell All

+ if(this.state.launchRequest){ + return ( +
+
+

Tell All

+
+ + +
- - - - -
- ); - } - - handleIntentClick(selectedIntent, index){ - this.setState({selectedIntent:selectedIntent, selectedIndex: index}); - } - - handleDeleteIntentClick(selectedIntent){ - if (selectedIntent._id){ - deleteIntent(selectedIntent._id).then(l=>l.text()).then(result=>{ - if (JSON.parse(result).n===1){ - let id = this.state.allIntents.indexOf(selectedIntent); - if (id!==-1){ - this.state.allIntents.splice(id,1); - this.setState({allIntents: this.state.allIntents, selectedIntent: {questions:[''],answer:''}}); - }else{ - alert("Error"); - } - }else{ - alert("Error"); - } - }); + ); + }else{ + return ( +
+
+

Tell All

+
+ + + + +
+ ); } } - handleSaveIntentClick(selectedIntent){ - updateIntent(selectedIntent._id, selectedIntent).then(l=>l.text()).then(result=>{ - if (JSON.parse(result).nModified===1){ - this.state.allIntents.map((intent,index)=>{ - if (intent._id === selectedIntent._id){ - let newAllIntents = this.state.allIntents; - newAllIntents[index] = selectedIntent; - this.setState({allIntents: newAllIntents, selectedIntent: selectedIntent}); - return 1; - } + createSkill(intents, name, answer){ + return { + _id: this.state._id, + skillID: this.state.skillID, + intents: intents, + invocationName: (name===undefined) ? this.state.invocationName : name, + invocationAnswer: (answer===undefined)? this.state.invocationAnswer: answer + }; + } + + handleIntentClick(selectedIntent, index){ + this.setState({selectedIntent:selectedIntent, selectedIndex: index, launchRequest:false}); + } + + handleLaunchRequestClick(){ + this.setState({selectedIndex: -2, launchRequest:true}); + } + + handleSaveLaunchRequestClick(name, answer){ + this.setState({invocationName: name, invocationAnswer: answer}); + try{ + updateSkill(this.createSkill(this.state.allIntents,name,answer)).then(l=>l.text()).then(result=>{ + if (JSON.parse(result).nModified!==1) + alert("Error"); + }); + }catch(e){ + alert("exception"); + } + } + + handleDeleteIntentClick(selectedIntent){ + let id = this.state.allIntents.indexOf(selectedIntent); + if (id!==-1){ + try{ + //I don't like this, state in database is different than component state, for some time + //TODO : move database operation in componentWillUpdate or componentDidUpdate + let newAllIntents = this.state.allIntents; + newAllIntents.splice(id,1); + this.setState({allIntents: newAllIntents, selectedIntent: {intentName:'', questions:[''],answer:''}}); + updateSkill(this.createSkill(newAllIntents)).then(l=>l.text()).then(result=>{ + if (JSON.parse(result).nModified!==1) + alert("Error"); }); - }else{ - alert("error - update went wrong"); + }catch(e){ + alert("exception"); } - }); + } + } + + + handleSaveIntentClick(selectedIntent){ + let newAllIntents = this.state.allIntents; + if (this.state.selectedIndex === -1){ + //new intent + newAllIntents.push(selectedIntent); + this.setState({allIntents: newAllIntents, selectedIntent: selectedIntent, selectedIndex: newAllIntents.length-1}); + }else{ + newAllIntents[this.state.selectedIndex] = selectedIntent; + this.setState({allIntents: newAllIntents, selectedIntent: selectedIntent}); + } + try{ + updateSkill(this.createSkill(newAllIntents)).then(l=>l.text()).then(result=>{ + if (JSON.parse(result).nModified!==1) + alert("Error"); + }); + }catch(e){ + alert("exception"); + } } handleAddIntentClick(){ - this.setState({allIntents: this.state.allIntents, selectedIntent: {questions:[''], answer:''}}); + this.setState({allIntents: this.state.allIntents, selectedIndex: -1,launchRequest:false,selectedIntent: {intentName:'',questions:[''], answer:''}}); } } diff --git a/web/src/components/IntentDetails.js b/web/src/components/IntentDetails.js index 52af133..9eb0b0d 100644 --- a/web/src/components/IntentDetails.js +++ b/web/src/components/IntentDetails.js @@ -1,6 +1,7 @@ import React, { Component } from 'react'; import {Button, SVGIcon, TextField} from 'react-md'; import '../css/Intent.css' +import {QUESTION_MAX_LENGTH, ANSWER_MAX_LENGTH, INTENT_NAME_MAX_LENGTH} from '../config'; class IntentDetails extends Component { constructor(props){ @@ -10,6 +11,9 @@ class IntentDetails extends Component { this.addQuestion = this.addQuestion.bind(this); this.deleteQuestion = this.deleteQuestion.bind(this); + this.handleQuestionEdit = this.handleQuestionEdit.bind(this); + this.handleAnswerEdit = this.handleAnswerEdit.bind(this); + this.handleIntentNameEdit = this.handleIntentNameEdit.bind(this); } componentWillReceiveProps(props){ @@ -19,18 +23,32 @@ class IntentDetails extends Component { render() { return (
+
+ +
Questions
{ this.state.intent.questions.map((question, index)=>{ return (
{this.deleteQuestion(question)}}> } + onChange={(e)=>{this.handleQuestionEdit(e,index)}} value={question}/>
); @@ -40,12 +58,14 @@ class IntentDetails extends Component { {
} @@ -60,7 +80,7 @@ class IntentDetails extends Component { addQuestion(){ let newIntent = this.state.intent; - newIntent.questions.push('New question'); + newIntent.questions.push(''); this.setState({intent: newIntent}); } @@ -72,6 +92,28 @@ class IntentDetails extends Component { this.setState({intent: newIntent}); } + + handleQuestionEdit(e,index){ + if (e.length === QUESTION_MAX_LENGTH) return; + let newIntent = this.state.intent; + newIntent.questions[index] = e; + this.setState({intent: newIntent}); + } + + handleAnswerEdit(e){ + if (e.length === ANSWER_MAX_LENGTH) return; + let newIntent = this.state.intent; + newIntent.answer = e; + this.setState({intent: newIntent}); + } + + handleIntentNameEdit(e){ + if (e.length === INTENT_NAME_MAX_LENGTH) return; + //e.replace(/\s/g,''); + let newIntent = this.state.intent; + newIntent.intentName = e; + this.setState({intent: newIntent}); + } } export default IntentDetails; diff --git a/web/src/components/IntentItem.js b/web/src/components/IntentItem.js index 45f5185..a4fb43b 100644 --- a/web/src/components/IntentItem.js +++ b/web/src/components/IntentItem.js @@ -1,6 +1,7 @@ import React, { Component } from 'react'; import {Button} from 'react-md'; import '../css/Intent.css' +import {INTENT_TITLE_MAX_LENGTH, INTENT_TITLE_TOOLTIP_DELAY} from '../config' class IntentItem extends Component { @@ -11,18 +12,18 @@ class IntentItem extends Component { } render() { - /* -
{this.state.onClick(this.state.intent,this.state.index)}}> - -

{this.state.index+1}. {this.state.intent.questions[0]}

-
- */ + let buttonTitle = this.state.intent.intentName; + if (buttonTitle.length > INTENT_TITLE_MAX_LENGTH){ + buttonTitle = this.state.intent.intentName.substr(0,INTENT_TITLE_MAX_LENGTH-1) + '. . .'; + } return (


diff --git a/web/src/components/IntentList.js b/web/src/components/IntentList.js index 264c2f3..b97f990 100644 --- a/web/src/components/IntentList.js +++ b/web/src/components/IntentList.js @@ -17,6 +17,8 @@ class IntentList extends Component { render() { return (
+

Intents

@@ -30,7 +32,7 @@ class IntentList extends Component { }) }

- +
); } diff --git a/web/src/components/LaunchRequest.js b/web/src/components/LaunchRequest.js new file mode 100644 index 0000000..28d5099 --- /dev/null +++ b/web/src/components/LaunchRequest.js @@ -0,0 +1,64 @@ +import React, { Component } from 'react'; +import {Button, TextField} from 'react-md'; +import '../css/Intent.css' +import { INVOCATION_NAME_MAX_LENGTH, INVOCATION_ANSWER_MAX_LENGTH } from '../config'; + +class LaunchRequest extends Component { + constructor(props){ + super(props); + + this.state = {invocationName: props.invocationName, invocationAnswer: props.invocationAnswer}; + + this.handleNameEdit = this.handleNameEdit.bind(this); + this.handleAnswerEdit = this.handleAnswerEdit.bind(this); + } + + componentWillReceiveProps(props){ + this.setState({invocationName: props.invocationName, invocationAnswer: props.invocationAnswer}); + } + + render() { + return ( +
+
Invocation name customers use to activate the skill. For example "Open Saburly" or "Talk to Saburly"
+ +

+
Answer customers get from Alexa when they activate the skill.
+ +

+

+ +
+ ); + } + + handleNameEdit(e){ + if (e.length === INVOCATION_NAME_MAX_LENGTH) return; + this.setState({invocationName: e}); + } + + handleAnswerEdit(e){ + if (e.length === INVOCATION_ANSWER_MAX_LENGTH) return; + this.setState({invocationAnswer: e}); + } +} + +export default LaunchRequest; diff --git a/web/src/config.js b/web/src/config.js index 0fefd14..1a4de76 100644 --- a/web/src/config.js +++ b/web/src/config.js @@ -1 +1,11 @@ -export const BASE_URL = 'localhost:5000'; \ No newline at end of file +export const BASE_URL = 'localhost:5000'; + +export const INTENT_NAME_MAX_LENGTH = 30; +export const QUESTION_MAX_LENGTH = 150; +export const ANSWER_MAX_LENGTH = 150; + +export const INTENT_TITLE_MAX_LENGTH = 20; +export const INTENT_TITLE_TOOLTIP_DELAY = 700; + +export const INVOCATION_NAME_MAX_LENGTH = 15; +export const INVOCATION_ANSWER_MAX_LENGTH = 100; diff --git a/web/src/css/Intent.css b/web/src/css/Intent.css index 490da92..62b35e2 100644 --- a/web/src/css/Intent.css +++ b/web/src/css/Intent.css @@ -4,13 +4,26 @@ min-height: 9999px; height: 9999px; float: left; - background-color: #d5dae2; } + background-color: #eff0f0; } .IntentList-title { font-size: 1.5em; height: 70px; padding: 20px; - text-align: left; } + text-align: left; + background-color: #eff0f0; } + +.LaunchRequest { + text-align: left; + width: 100%; + height: 50px; + background-color: #d8d8d8; } + +.LaunchRequest-selected { + text-align: left; + width: 100%; + height: 50px; + background-color: #f5f5f5; } .AddIntent { float: right; @@ -22,7 +35,7 @@ height: 50px; width: 100%; text-align: left; - background-color: #adb0b5; } + background-color: #d8d8d8; } .IntentItem-selected { margin-top: 2px; @@ -30,7 +43,7 @@ height: 50px; width: 100%; text-align: left; - background-color: #e8ecf2; } + background-color: #f5f5f5; } /*IntentDetails CSS*/ .IntentDetails { @@ -38,7 +51,21 @@ width: 70%; min-height: 9999px; height: 9999px; - background-color: #e8ecf2; } + background-color: #f5f5f5; } .QuestionBox { margin: 25px; } + +/*LaunchRequest CSS*/ +.LaunchRequestBox { + float: left; + width: 70%; + min-height: 9999px; + height: 9999px; + background-color: #f5f5f5; } + +.ExplanationText { + float: left; + margin-top: 30px; + margin-left: 20px; + text-align: left; } diff --git a/web/src/css/Intent.scss b/web/src/css/Intent.scss index c954fe7..b35497e 100644 --- a/web/src/css/Intent.scss +++ b/web/src/css/Intent.scss @@ -5,7 +5,7 @@ min-height:9999px; height:9999px; float:left; - background-color: #d5dae2; + background-color: #eff0f0; } .IntentList-title{ @@ -13,6 +13,21 @@ height: 70px; padding: 20px; text-align:left; + background-color: #eff0f0; +} + +.LaunchRequest{ + text-align: left; + width: 100%; + height: 50px; + background-color: #d8d8d8 +} + +.LaunchRequest-selected{ + text-align: left; + width: 100%; + height: 50px; + background-color: #f5f5f5; } .AddIntent{ @@ -26,7 +41,7 @@ height: 50px; width:100%; text-align:left; - background-color: #adb0b5; + background-color: #d8d8d8; } .IntentItem-selected{ @@ -35,7 +50,7 @@ height: 50px; width:100%; text-align:left; - background-color:#e8ecf2; + background-color: #f5f5f5; } @@ -46,9 +61,26 @@ width: 70%; min-height:9999px; height:9999px; - background-color:#e8ecf2; + background-color: #f5f5f5; } .QuestionBox{ margin:25px; +} + +/*LaunchRequest CSS*/ + +.LaunchRequestBox{ + float: left; + width:70%; + min-height: 9999px; + height: 9999px; + background-color: #f5f5f5; +} + +.ExplanationText{ + float: left; + margin-top: 30px; + margin-left: 20px; + text-align: left; } \ No newline at end of file diff --git a/web/src/lib/api.js b/web/src/lib/api.js index 6891bef..06964bb 100644 --- a/web/src/lib/api.js +++ b/web/src/lib/api.js @@ -1,24 +1,31 @@ import fetch from 'isomorphic-fetch'; import {BASE_URL} from '../config'; -export const getAllIntents = ()=>{ - let url = `http://${BASE_URL}/intents` +export const getAllIntents = (id)=>{ + let url = `http://${BASE_URL}/intents/${id}` return fetch(url, {method: 'GET'}); } -export const deleteIntent = id=>{ - let url = `http://${BASE_URL}/deleteIntent/${id}` +export const getSkill = (id)=>{ + let url = `http://${BASE_URL}/getSkill/${id}` return fetch(url, {method: 'GET'}); } -export const updateIntent = (id,intent)=>{ - let url = `http://${BASE_URL}/updateIntent/${id}` +export const deleteSkill = (id)=>{ + let url = `http://${BASE_URL}/deleteSkill/${id}` + return fetch(url, {method: 'GET'}); +} + +export const updateSkill = (skill)=>{ + console.log(skill); + let id = (skill._id) ? skill._id : -1; + let url = `http://${BASE_URL}/updateSkill/${id}` return fetch(url, { method: 'POST', headers: { 'Accept': 'application/json', 'Content-Type': 'application/json' }, - body: JSON.stringify(intent), + body: JSON.stringify(skill), }); } \ No newline at end of file From 74ee9de93fbe619dd9c445850510de7a88fffbb0 Mon Sep 17 00:00:00 2001 From: GotPPay Date: Sun, 3 Dec 2017 00:15:11 +0100 Subject: [PATCH 02/83] generate interaction model --- backend/express.js | 3 ++- backend/helpers/amazon.js | 51 ++++++++++++++++++++++++++++++++++++--- web/src/App.js | 5 ++-- 3 files changed, 53 insertions(+), 6 deletions(-) diff --git a/backend/express.js b/backend/express.js index e40491a..dbf6b6f 100644 --- a/backend/express.js +++ b/backend/express.js @@ -1,4 +1,4 @@ -import { sendUpdateToAmazon } from './helpers/amazon'; +var amazonHelper = require('./helpers/amazon'); var express = require('express'); var alexa = require('alexa-app'); @@ -60,6 +60,7 @@ router.post ('/updateSkill/:id', async (req, res, next) => { if (id !== '-1'){ let result = db.collection('skill_list').update({_id: ObjectID(id)}, skill,{upsert:true}, (err, result)=>{ if (err) throw(err); + amazonHelper.sendUpdateToAmazon(id, db); res.json(result); }); }else{ diff --git a/backend/helpers/amazon.js b/backend/helpers/amazon.js index 7789579..4466cf5 100644 --- a/backend/helpers/amazon.js +++ b/backend/helpers/amazon.js @@ -1,7 +1,52 @@ +var ObjectID = require ('mongodb').ObjectID; +var generateInteractionModel = function (skill) { + try{ + let result = {}; + let allIntents = []; + let defaultIntents = [{ + name: "AMAZON.CancelIntent", + samples: [] + }, + { + name: "AMAZON.HelpIntent", + samples: [] + }, + { + name: "AMAZON.StopIntent", + samples: [] + }]; + defaultIntents.map(intent=>{ + allIntents.push(intent); + }); -export const sendUpdateToAmazon = (id)=>{ - //id - skill ID in database + skill.intents.map(intent=>{ + allIntents.push({name: intent.intentName, samples: intent.questions, slots: []}); + }); -} \ No newline at end of file + result.languageModel = { + invocationName: skill.invocationName, + intents: allIntents + }; + + return JSON.stringify(result); + }catch(e){ + console.log("error generate : " + e); + } +} + +module.exports = { + sendUpdateToAmazon: function (id, db){ + db.collection ('skill_list').find({_id: ObjectID(id)}).toArray((err,result)=>{ + if (err){ + console.log("Error finding skill"); + }else{ + //generateInteractionModel + //POST to amazon + let generatedInteractionModel = generateInteractionModel(result[0]); + + } + }); + } +}; \ No newline at end of file diff --git a/web/src/App.js b/web/src/App.js index 7761b92..6928119 100644 --- a/web/src/App.js +++ b/web/src/App.js @@ -11,8 +11,9 @@ class App extends Component { constructor(props){ super(props); - this.state={_id:'5a22ffd36ce046c749739453', + this.state={_id:'5a232fb86ce046c749739455', skillID:'', + skillName:'', invocationName:'Saburly', invocationAnswer:'We are saburly', allIntents:[], @@ -23,7 +24,7 @@ class App extends Component { getSkill(this.state._id).then(l=> l.text()).then(result=>{ let jResult = JSON.parse(result)[0]; if (jResult===undefined) return; - this.setState({ skillID:jResult.skillID, invocationName: jResult.invocationName, + this.setState({ skillID:jResult.skillID,skillName:jResult.skillName, invocationName: jResult.invocationName, invocationAnswer: jResult.invocationAnswer, allIntents: jResult.intents}) }) From 94d2883c016b8155c2fb00f9767e17a88b9fb401 Mon Sep 17 00:00:00 2001 From: GotPPay Date: Sun, 3 Dec 2017 17:38:58 +0100 Subject: [PATCH 03/83] send update to amazon --- backend/express.js | 25 ++++++++- backend/helpers/amazon.js | 92 ++++++++++++++++++-------------- backend/package.json | 3 +- web/src/App.js | 18 ++++--- web/src/components/IntentItem.js | 2 +- web/src/lib/api.js | 1 - 6 files changed, 89 insertions(+), 52 deletions(-) diff --git a/backend/express.js b/backend/express.js index dbf6b6f..b09669e 100644 --- a/backend/express.js +++ b/backend/express.js @@ -1,5 +1,7 @@ var amazonHelper = require('./helpers/amazon'); +require('isomorphic-fetch'); + var express = require('express'); var alexa = require('alexa-app'); @@ -7,6 +9,9 @@ var bodyParser = require('body-parser'); const dbURL = 'mongodb://localhost:27017/tellall'; const PORT = 5000; +const TOKEN = 'Atza|IwEBIMv0spn-eZhjP8-R2Jjkb4VUi-EY4V3MX-wlJyr2P6YBUmIChl7VKgRberEdQ-Wolp53SqfwHbxlSo4-rdUgFYFAxImB622boeqUBBCtwybP0OTBJBT1CVm_FBr48Li5LkwR9DxPciCnc052ddohpjGuGZxsCqIJo2c-7LPEhH0Lx63VOFgfPIfBsrVqjDbPNrr5ApPUijKYZWurktb6ytTks8eFUSTyv3FDbE5HEng0qpE4mgSjdgBkEc8BgUfpm2QGvctINH9SioUJOJonxgTPYhbD4BEd-jdQHsltKFzkb3-Rm3lFhEu3_CQFxCeQ6yGe1the_qID3vnPfpSY6hyblgF5L_5d7bE7BWg8fm6rSwXv67L2sMwBOji6cuR0wVVvXfYxmgGIMGkVQrq-SPSdtCpx2BvBz2SqqN6a_98svP8ukvhwc_oJsN6VwEmTDFgutNf-XuGkuVii-k9-DncwuwD00LvJG1FhBvbvSyuv-a3LAJSqTmroemTDG0xzLn7ULFY5p-93sM0_ZNGFeW-lL_r1ldqM_5lFRKDta1Tkg7lT9-rHftgnpGs4zv7vGLIPzHpNiXjsKyCk-wMQrihhWlR15kiz7oKDeTf6wCIETg'; +const USER_ID = 'amzn1.ask.account.AGE34MG3VIQ75D4Q5CXFK25GUOXHZ2MIVSII3QUCOT3CP44IPE25PLD3DWW7HLWO2KVC7PC7VUXSZZEFPVHJ6PTDYVYYESJE35CE6VEXCE3BI3AK24TGO4CJXFF3SZ7IA4QVJRZC6EN4MUF2WUP4IB4CGDLRZZMYQENOFNBYWFXZHMZ5PA6S6ERK7NGEPUSDHXWKH26UGMIATMI'; + var MongoClient = require ('mongodb').MongoClient; @@ -58,10 +63,26 @@ router.post ('/updateSkill/:id', async (req, res, next) => { let skill = req.body; delete skill._id; if (id !== '-1'){ + let completeResult = {databaseUpdate:false, amazonUpdate:false, amazonMessage:''}; + let result = db.collection('skill_list').update({_id: ObjectID(id)}, skill,{upsert:true}, (err, result)=>{ if (err) throw(err); - amazonHelper.sendUpdateToAmazon(id, db); - res.json(result); + + completeResult.databaseUpdate = (JSON.parse(result).nModified===1); + + let generatedInteractionModel = amazonHelper.generateInteractionModel(skill); + + fetch(`https://api.amazonalexa.com/v0/skills/${skill.skillID}/interactionModel/locales/en-US`, { + method: 'POST', + headers: { + Authorization: TOKEN + }, + body: generatedInteractionModel + }).then(l=>l.text()).then(result=>{ + completeResult.amazonUpdate = (JSON.stringify(result)===JSON.stringify({})); + completeResult.amazonMessage = JSON.stringify(result); + res.json(completeResult); + }); }); }else{ //no new skills for now diff --git a/backend/helpers/amazon.js b/backend/helpers/amazon.js index 4466cf5..2ebcf9f 100644 --- a/backend/helpers/amazon.js +++ b/backend/helpers/amazon.js @@ -1,52 +1,62 @@ +require('isomorphic-fetch'); + var ObjectID = require ('mongodb').ObjectID; -var generateInteractionModel = function (skill) { + +var getBuildStatus = function(skillID){ try{ - let result = {}; - let allIntents = []; - let defaultIntents = [{ - name: "AMAZON.CancelIntent", - samples: [] - }, - { - name: "AMAZON.HelpIntent", - samples: [] - }, - { - name: "AMAZON.StopIntent", - samples: [] - }]; - - defaultIntents.map(intent=>{ - allIntents.push(intent); + fetch(`https://api.amazonalexa.com/v0/skills/${skillID}/interactionModel/locales/en-US/status`, { + method: 'GET', + headers: { + Authorization: TOKEN + }, + }).then(l=>l.text()).then(result=>{ + return result; }); - - skill.intents.map(intent=>{ - allIntents.push({name: intent.intentName, samples: intent.questions, slots: []}); - }); - - result.languageModel = { - invocationName: skill.invocationName, - intents: allIntents - }; - - return JSON.stringify(result); }catch(e){ - console.log("error generate : " + e); + console.log("err : " + e); } } module.exports = { - sendUpdateToAmazon: function (id, db){ - db.collection ('skill_list').find({_id: ObjectID(id)}).toArray((err,result)=>{ - if (err){ - console.log("Error finding skill"); - }else{ - //generateInteractionModel - //POST to amazon - let generatedInteractionModel = generateInteractionModel(result[0]); - - } - }); + generateInteractionModel: function(skill){ + try{ + let result = {}; + let allIntents = []; + let defaultIntents = [{ + name: "AMAZON.CancelIntent", + samples: [] + }, + { + name: "AMAZON.HelpIntent", + samples: [] + }, + { + name: "AMAZON.StopIntent", + samples: [] + }]; + + /* + defaultIntents.map(intent=>{ + allIntents.push(intent); + }); + */ + + skill.intents.map(intent=>{ + allIntents.push({name: intent.intentName, samples: intent.questions}); + }); + + result.interactionModel = {}; + + result.interactionModel.languageModel = { + invocationName: skill.invocationName, + intents: allIntents + }; + + return JSON.stringify(result); + }catch(e){ + console.log("error generate : " + e); + } } + }; \ No newline at end of file diff --git a/backend/package.json b/backend/package.json index de51119..a496860 100644 --- a/backend/package.json +++ b/backend/package.json @@ -4,10 +4,11 @@ "description": "", "main": "test.js", "dependencies": { + "alexa-app": "4.2.0", "body-parser": "^1.13.1", "ejs": "^2.3.1", "express": "^4.13.0", - "alexa-app": "4.2.0" + "isomorphic-fetch": "^2.2.1" }, "author": "Matt Kruse (http://mattkruse.com/)", "license": "MIT" diff --git a/web/src/App.js b/web/src/App.js index 6928119..e2b5455 100644 --- a/web/src/App.js +++ b/web/src/App.js @@ -99,8 +99,10 @@ class App extends Component { this.setState({invocationName: name, invocationAnswer: answer}); try{ updateSkill(this.createSkill(this.state.allIntents,name,answer)).then(l=>l.text()).then(result=>{ - if (JSON.parse(result).nModified!==1) - alert("Error"); + let jResult = JSON.parse(result); + if (!(jResult.databaseUpdate && jResult.amazonUpdate)){ + alert('Database update : ' + jResult.databaseUpdate + '\r\nAmazon update : ' + jResult.amazonUpdate + '\r\nMessage : ' + jResult.amazonMessage); + } }); }catch(e){ alert("exception"); @@ -117,8 +119,10 @@ class App extends Component { newAllIntents.splice(id,1); this.setState({allIntents: newAllIntents, selectedIntent: {intentName:'', questions:[''],answer:''}}); updateSkill(this.createSkill(newAllIntents)).then(l=>l.text()).then(result=>{ - if (JSON.parse(result).nModified!==1) - alert("Error"); + let jResult = JSON.parse(result); + if (!(jResult.databaseUpdate && jResult.amazonUpdate)){ + alert('Database update : ' + jResult.databaseUpdate + '\r\nAmazon update : ' + jResult.amazonUpdate + '\r\nMessage : ' + jResult.amazonMessage); + } }); }catch(e){ alert("exception"); @@ -139,8 +143,10 @@ class App extends Component { } try{ updateSkill(this.createSkill(newAllIntents)).then(l=>l.text()).then(result=>{ - if (JSON.parse(result).nModified!==1) - alert("Error"); + let jResult = JSON.parse(result); + if (!(jResult.databaseUpdate && jResult.amazonUpdate)){ + alert('Database update : ' + jResult.databaseUpdate + '\r\nAmazon update : ' + jResult.amazonUpdate + '\r\nMessage : ' + jResult.amazonMessage); + } }); }catch(e){ alert("exception"); diff --git a/web/src/components/IntentItem.js b/web/src/components/IntentItem.js index a4fb43b..435da7a 100644 --- a/web/src/components/IntentItem.js +++ b/web/src/components/IntentItem.js @@ -22,7 +22,7 @@ class IntentItem extends Component { onClick={()=>{this.state.onClick(this.state.intent,this.state.index)}} flat tooltipDelay={INTENT_TITLE_TOOLTIP_DELAY} - tooltipLabel={this.state.intent.questions[0].length>INTENT_TITLE_MAX_LENGTH ? this.state.intent.questions[0] : ''}> + tooltipLabel={this.state.intent.intentName.length>INTENT_TITLE_MAX_LENGTH ? this.state.intent.questions[0] : ''}> {this.state.index+1}. {buttonTitle}

diff --git a/web/src/lib/api.js b/web/src/lib/api.js index 06964bb..229cbf3 100644 --- a/web/src/lib/api.js +++ b/web/src/lib/api.js @@ -17,7 +17,6 @@ export const deleteSkill = (id)=>{ } export const updateSkill = (skill)=>{ - console.log(skill); let id = (skill._id) ? skill._id : -1; let url = `http://${BASE_URL}/updateSkill/${id}` return fetch(url, { From e8ee77a40d55a757ff96840fec2570a951346fa5 Mon Sep 17 00:00:00 2001 From: GotPPay Date: Sun, 3 Dec 2017 18:25:10 +0100 Subject: [PATCH 04/83] remove unused items --- backend/config.js | 2 - backend/lambda.js | 14 ----- backend/template.js | 116 ----------------------------------------- backend/test.js | 79 ---------------------------- backend/views/test.ejs | 10 ---- 5 files changed, 221 deletions(-) delete mode 100644 backend/config.js delete mode 100644 backend/lambda.js delete mode 100644 backend/template.js delete mode 100644 backend/test.js delete mode 100644 backend/views/test.ejs diff --git a/backend/config.js b/backend/config.js deleted file mode 100644 index 0c1ae6b..0000000 --- a/backend/config.js +++ /dev/null @@ -1,2 +0,0 @@ -export const dbURL = 'mongodb://localhost:27017/tellall'; -export const PORT = 5000; \ No newline at end of file diff --git a/backend/lambda.js b/backend/lambda.js deleted file mode 100644 index 35bd0c5..0000000 --- a/backend/lambda.js +++ /dev/null @@ -1,14 +0,0 @@ -var alexa = require("alexa-app"); -var find = require("find-my-iphone"); - -var app = new alexa.app(); -app.launch(function(request, response) { - find("me@icloud.com", "mypassword", "iPhone", function() { - response.say("OK").send(); - }); - // because this is an async handler - return false; -}); - -// connect to lambda -exports.handler = app.lambda(); diff --git a/backend/template.js b/backend/template.js deleted file mode 100644 index 66f536a..0000000 --- a/backend/template.js +++ /dev/null @@ -1,116 +0,0 @@ -var template = {}; -// LaunchRequest template -template.launch = { - "version": "1.0", - "session": { - "new": true, - "sessionId": "amzn1.echo-api.session.abeee1a7-aee0-41e6-8192-e6faaed9f5ef", - "attributes": {}, - "application": { - "applicationId": "amzn1.ask.skill.7115bfc9-313e-4728-830b-ebd19ce96cb3" - - }, - "user": { - "userId": "amzn1.account.AM3B227HF3FAM1B261HK7FFM3A2" - } - }, - "request": { - "type": "LaunchRequest", - "requestId": "amzn1.echo-api.request.9cdaa4db-f20e-4c58-8d01-c75322d6c423" - } -}; -// IntentRequest template -template.intent = { - "version": "1.0", - "session": { - "new": false, - "sessionId": "amzn1.echo-api.session.abeee1a7-aee0-41e6-8192-e6faaed9f5ef", - "attributes": {}, - "application": { - "applicationId": "amzn1.ask.skill.7115bfc9-313e-4728-830b-ebd19ce96cb3" - }, - "user": { - "userId": "amzn1.account.AM3B227HF3FAM1B261HK7FFM3A2" - } - }, - "request": { - "type": "IntentRequest", - "requestId": "amzn1.echo-api.request.6919844a-733e-4e89-893a-fdcb77e2ef0d", - "intent": { - "name": "sampleIntent", - "slots": { - "NAME": { - "name": "NAME", - "value": "Matt" - } - } - } - } -}; -// errorIntent template -template.errorIntent = { - "version": "1.0", - "session": { - "new": false, - "sessionId": "amzn1.echo-api.session.abeee1a7-aee0-41e6-8192-e6faaed9f5ef", - "attributes": {}, - "application": { - "applicationId": "amzn1.ask.skill.7115bfc9-313e-4728-830b-ebd19ce96cb3" - }, - "user": { - "userId": "amzn1.account.AM3B227HF3FAM1B261HK7FFM3A2" - } - }, - "request": { - "type": "IntentRequest", - "requestId": "amzn1.echo-api.request.6919844a-733e-4e89-893a-fdcb77e2ef0d", - "intent": { - "name": "errorIntent", - "slots": {} - } - } -}; -// missingIntent template -template.missingIntent = { - "version": "1.0", - "session": { - "new": false, - "sessionId": "amzn1.echo-api.session.abeee1a7-aee0-41e6-8192-e6faaed9f5ef", - "attributes": {}, - "application": { - "applicationId": "amzn1.ask.skill.7115bfc9-313e-4728-830b-ebd19ce96cb3" - }, - "user": { - "userId": "amzn1.account.AM3B227HF3FAM1B261HK7FFM3A2" - } - }, - "request": { - "type": "IntentRequest", - "requestId": "amzn1.echo-api.request.6919844a-733e-4e89-893a-fdcb77e2ef0d", - "intent": { - "name": "missingIntent", - "slots": {} - } - } -}; -// SessionEndedRequest template -template.session_end = { - "version": "1.0", - "session": { - "new": false, - "sessionId": "amzn1.echo-api.session.abeee1a7-aee0-41e6-8192-e6faaed9f5ef", - "attributes": {}, - "application": { - "applicationId": "amzn1.ask.skill.7115bfc9-313e-4728-830b-ebd19ce96cb3" - }, - "user": { - "userId": "amzn1.account.AM3B227HF3FAM1B261HK7FFM3A2" - } - }, - "request": { - "type": "SessionEndedRequest", - "requestId": "amzn1.echo-api.request.d8c37cd6-0e1c-458e-8877-5bb4160bf1e1", - "reason": "USER_INITIATED" - } -}; -module.exports = template; diff --git a/backend/test.js b/backend/test.js deleted file mode 100644 index ef4e7c5..0000000 --- a/backend/test.js +++ /dev/null @@ -1,79 +0,0 @@ -var alexa = require("alexa-app"); -var template = require("./template.js"); - -var app = new alexa.app("test"); - -app.dictionary = { - "names": ["Bob", "Jack", "Matt", "Mary", "Jane", "Bill"] -}; - -app.launch(function(request, response) { - response.say("App launched!"); -}); - -app.intent("sampleIntent", { - "slots": { "NAME": "LITERAL", "AGE": "NUMBER" }, - "utterances": ["my {name is|name's} {names|NAME} and {I am|I'm} {1-100|AGE}{ years old|}"] - }, - function(request, response) { - setTimeout(function() { - response.say("After timeout!").say(" test ").reprompt("Reprompt"); - response.send(); - }, 1000); - // We are async! - return false; - } -); - -app.intent("errorIntent", function(request, response) { - response.say(someVariableThatDoesntExist); -}); - -// output the schema -console.log("\n\nSCHEMA:\n\n" + app.schema() + "\n\n"); -// output sample utterances -console.log("\n\nUTTERANCES:\n\n" + app.utterances() + "\n\n"); - -// test pre() and post() functions -app.pre = function(request, response, type) { - response.say("This part of the output is from pre(). "); -}; -app.post = function(request, response, type, exception) { - if (exception) { - response.clear().say("An error occured: " + exception).send(); - } -}; - -// error example -app.request(template.errorIntent) - .then(function(response) { - console.log(JSON.stringify(response, null, 3)); - }); - -// async example -app.request(template.intent) - .then(function(response) { - console.log(JSON.stringify(response, null, 3)); - }); - -// synchronous example -app.request(template.launch) - .then(function(response) { - console.log(JSON.stringify(response, null, 3)); - }); - -// error example -app.messages.NO_INTENT_FOUND = "Why you called dat intent? I don't know bout dat"; -app.request(template.missingIntent) - .then(function(response) { - console.log(JSON.stringify(response, null, 3)); - }); - -// error handler example -app.error = function(e, request, response) { - response.say("I captured the exception! It was: " + e.message); -}; -app.request(template.errorIntent) - .then(function(response) { - console.log(JSON.stringify(response, null, 3)); - }); diff --git a/backend/views/test.ejs b/backend/views/test.ejs deleted file mode 100644 index a84c9fe..0000000 --- a/backend/views/test.ejs +++ /dev/null @@ -1,10 +0,0 @@ -
-Schema: - -<%=schema%> - -Utterances: - -<%=utterances%> - -
\ No newline at end of file From 124885b41d2408dfedf7fc92061fc87af0b7a904 Mon Sep 17 00:00:00 2001 From: GotPPay Date: Sun, 3 Dec 2017 18:47:24 +0100 Subject: [PATCH 05/83] clean backend --- backend/express.js | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/backend/express.js b/backend/express.js index b09669e..6e2bc7e 100644 --- a/backend/express.js +++ b/backend/express.js @@ -118,21 +118,6 @@ alexaApp.express({ // now POST calls to /test in express will be handled by the app.request() function // from here on you can setup any other express routes or middlewares as normal -app.set("view engine", "ejs"); - -alexaApp.launch(function(request, response) { - response.say("You launched Saburly app!"); -}); - -alexaApp.intent("GetProcessIntent", { - "utterances": [ - "tell me about projects", "say something about your project", "what are your projects" - ] - }, - function(request, response) { - response.say("We collaborate closely with our clients at each step of the developmentprocess. From designing the UX to developing the front-end andarchitecting the back-end."); - } -); app.use (function (req, res, next) { res.header ('Access-Control-Allow-Origin', '*'); From 9652645339b561c64147984c238a90b4b04bbc5d Mon Sep 17 00:00:00 2001 From: GotPPay Date: Mon, 4 Dec 2017 12:18:05 +0100 Subject: [PATCH 06/83] fix --- backend/SaburlySkillInteractionModel.json | 60 +++++++++++++++++++++++ backend/express.js | 46 ++++++++++++++--- 2 files changed, 100 insertions(+), 6 deletions(-) create mode 100644 backend/SaburlySkillInteractionModel.json diff --git a/backend/SaburlySkillInteractionModel.json b/backend/SaburlySkillInteractionModel.json new file mode 100644 index 0000000..ff9fdd7 --- /dev/null +++ b/backend/SaburlySkillInteractionModel.json @@ -0,0 +1,60 @@ +{ + "languageModel": { + "intents": [ + { + "name": "AMAZON.CancelIntent", + "samples": [] + }, + { + "name": "AMAZON.HelpIntent", + "samples": [] + }, + { + "name": "AMAZON.StopIntent", + "samples": [] + }, + { + "name": "GetProcessIntent", + "samples": [ + "tell me your process", + "what is your process", + "how do you work", + "about your proces" + ], + "slots": [] + }, + { + "name": "GetProjectsIntent", + "samples": [ + "list me your projects", + "show me what you have done", + "what did you do so far", + "what are your projects", + "your project" + ], + "slots": [] + }, + { + "name": "GetServicesIntent", + "samples": [ + "what are your services", + "what services do you ofer", + "what you do", + "your service", + "tell me service" + ], + "slots": [] + }, + { + "name": "GetTechnologiesIntent", + "samples": [ + "list me your technologies", + "give me your technologies", + "what you know" + ], + "slots": [] + } + ], + "invocationName": "saburly" + } +} \ No newline at end of file diff --git a/backend/express.js b/backend/express.js index 6e2bc7e..29e89b2 100644 --- a/backend/express.js +++ b/backend/express.js @@ -12,8 +12,6 @@ const PORT = 5000; const TOKEN = 'Atza|IwEBIMv0spn-eZhjP8-R2Jjkb4VUi-EY4V3MX-wlJyr2P6YBUmIChl7VKgRberEdQ-Wolp53SqfwHbxlSo4-rdUgFYFAxImB622boeqUBBCtwybP0OTBJBT1CVm_FBr48Li5LkwR9DxPciCnc052ddohpjGuGZxsCqIJo2c-7LPEhH0Lx63VOFgfPIfBsrVqjDbPNrr5ApPUijKYZWurktb6ytTks8eFUSTyv3FDbE5HEng0qpE4mgSjdgBkEc8BgUfpm2QGvctINH9SioUJOJonxgTPYhbD4BEd-jdQHsltKFzkb3-Rm3lFhEu3_CQFxCeQ6yGe1the_qID3vnPfpSY6hyblgF5L_5d7bE7BWg8fm6rSwXv67L2sMwBOji6cuR0wVVvXfYxmgGIMGkVQrq-SPSdtCpx2BvBz2SqqN6a_98svP8ukvhwc_oJsN6VwEmTDFgutNf-XuGkuVii-k9-DncwuwD00LvJG1FhBvbvSyuv-a3LAJSqTmroemTDG0xzLn7ULFY5p-93sM0_ZNGFeW-lL_r1ldqM_5lFRKDta1Tkg7lT9-rHftgnpGs4zv7vGLIPzHpNiXjsKyCk-wMQrihhWlR15kiz7oKDeTf6wCIETg'; const USER_ID = 'amzn1.ask.account.AGE34MG3VIQ75D4Q5CXFK25GUOXHZ2MIVSII3QUCOT3CP44IPE25PLD3DWW7HLWO2KVC7PC7VUXSZZEFPVHJ6PTDYVYYESJE35CE6VEXCE3BI3AK24TGO4CJXFF3SZ7IA4QVJRZC6EN4MUF2WUP4IB4CGDLRZZMYQENOFNBYWFXZHMZ5PA6S6ERK7NGEPUSDHXWKH26UGMIATMI'; - - var MongoClient = require ('mongodb').MongoClient; var ObjectID = require ('mongodb').ObjectID; @@ -43,7 +41,6 @@ router.get ('/getSkill/:id', async (req, res, next) => { } }); - router.get ('/deleteSkill/:skillID', async (req, res, next) => { try { let id = req.params.id; @@ -94,13 +91,13 @@ router.post ('/updateSkill/:id', async (req, res, next) => { } }); - - +var skillID = 'amzn1.ask.skill.7115bfc9-313e-4728-830b-ebd19ce96cb3'; +var skillDbID = '5a232fb86ce046c749739455'; //_id in database var app = express(); // ALWAYS setup the alexa app and attach it to express before anything else. -var alexaApp = new alexa.app('step3'); +var alexaApp = new alexa.app('step3'); // this means we still work with one skill alexaApp.express({ expressApp: app, @@ -119,6 +116,43 @@ alexaApp.express({ // from here on you can setup any other express routes or middlewares as normal +alexaApp.launch(function(request, response) { + const skill = db.collection('skill_list').findOne({_id: ObjectID(skillDbID)}, (err,result)=>{ + if (err){ + response.say("I could not find desired skill") + }else{ + response.say(skill.invocationAnswer); + } + }); +}); + +var findElementByAttr = function (data, attr, val){ + for(var i = 0; i < data.length; i ++) { + if(data[i][attr] === val) { + return i; + } + } + return -1; +} + +alexaApp.request = (jsonRequest) => { + const alexaRequest = new alexa.request(jsonRequest); + if (alexaRequest.type() === "IntentRequest") { + const skill = db.collection('skill_list').findOne({ + _id: skillDbID + }); + if (skill) { + let intentId = findElementByAttr(skill.intents, 'intentName', alexaRequest.data.request.intent.name); + const response = new alexa.response(alexaRequest.getSession()); + if (intentId !== -1){ + return response.say(skill.intents[intentId].answer); + }else{ + return response.say('Sorry, I could not find desired intent'); + } + } + } +}; + app.use (function (req, res, next) { res.header ('Access-Control-Allow-Origin', '*'); res.header ( From 32e0f4d6d719b17ccda413a0672ae9fbecf62eaf Mon Sep 17 00:00:00 2001 From: GotPPay Date: Fri, 5 Jan 2018 00:51:49 +0100 Subject: [PATCH 07/83] automatic token refresh, initial stage --- backend/config.js | 15 ++++ backend/express.js | 67 ++++++++--------- backend/helpers/amazon.js | 142 +++++++++++++++++++++++++----------- backend/helpers/database.js | 21 ++++++ backend/helpers/token.js | 19 +++++ backend/package.json | 3 +- web/src/App.js | 40 +++++----- 7 files changed, 208 insertions(+), 99 deletions(-) create mode 100644 backend/config.js create mode 100644 backend/helpers/database.js create mode 100644 backend/helpers/token.js diff --git a/backend/config.js b/backend/config.js new file mode 100644 index 0000000..ad1b06d --- /dev/null +++ b/backend/config.js @@ -0,0 +1,15 @@ +var config = {}; + +config.dbURL = 'mongodb://localhost:27017/tellall'; +config.PORT = 5000; + +config.TOKEN = 'Atza|IwEBIBe6gDqrrowEEav6N-_6s4NztYeP3oG8PGWmu8ZiZw6lbOh3wNla3TK6pY-VEpT1d8an-dVf_n3kXJzVFsNo_4xBfZyFHGoCTDTFjs3yBRul4PVdBOhwwiH3-sgRLcUofZbe2oE06GmTcbfYtaStfXpQI5dfpldfnsJg_CvhSA6AHb_snJT3F6lyXzbV076d_3cYUMJxFldJGnYcviNHHxjjmuQTD06hhGzCbAxxe9eBmkuopRsNfyedLT2UlKP_ublah9CUGA3AdIX_3Iuke82jMwGnNl9gv7pbaDNEjAbj7IQSl3B08uuREtJq-oTBOjALNXRvFxTJmQjZwXNf9eHC7fSHJDdEPdZQU0AcffRQObAyAkUuL6Jv39OHzhb3Q64-zzoyODqnJyLP5SQZ2JVF53Kc_cTBqjIc9pXljqe7yEVk6JDs7q1zKbBibx_AQm57TO79IzWyLBzBMlYL5HdTsqEfRzLeDw2tws-hGMgkx2HWfdbYnmf5Qb4SyIhzvmmdfPLg3MVKTxjIBu1rx0xf3n0PLZP1EO6jsJPoMRPg77Gm4oit5Zp6s37ek3A3Vxh-ntoASpkrkxGTG9kVtRNt'; +config.REFRESH_TOKEN = 'Atzr|IwEBICA3kDhfSJxlwvnQp9AD1o115AC_KBbFd5GBg8oN_QHWn2or5xFQ09BruuK6a07tGHtTt_9q2Y21mnOMH4RDtYXTVG9ADgLE6mHWKZFxYVwt3kHMiUJdY5lJcsOtWLoblrS-bJ0BEXXK5nVDt_bSI5IB7NUf-9QVZxhovRH_ANSxdTjJT0_rMIAZY3WEj68FEap49q_pg72BhnxHVZD2TC3zvX96_DN65HE5SoSgT7OiMAeiJewB_SyemW_HxBwaB-_X-G1ifOtnrzZ4gXTpOrEUlHI2YPuvRMBMtmz1h-nXDZYv3vwU3RA57Qj_ZNVcScj8_RXf2xq8w48v0PzZFXYBSalfnqPq6eUvSSbAJUp6bB8y596JlvR5dFQe_Z--X0Gkfo85IcyrI9D44vK9sJhrGhCVi2FDDa8pHczmNSen99JYZvDif-zpYzgbcymAkOV0gC7JvYMxlZfETT3NTBy7eVA7fJI1SZaeA_qW49xRcBkZBu5gkqTpeGWUU1cGr2aXRVVmXGM22NfV5E7KzvEBsCeHml_tCfxZeKY8Msd8hJb0Cd59u-_hsuc8oNjsOpIdFF976dY3uTmAgHWpG2PH'; +config.TOKEN_EXPIRES_IN = 1515100500; + +config.SKILL_ID = 'amzn1.ask.skill.efbf0564-a732-4ba9-958f-57939138adae'; + +config.CLIENT_ID = 'amzn1.application-oa2-client.c748ca56ded04a95b236979898585ff7'; +config.CLIENT_SECRET = '6dea8125cecd049d3c4cff7bb5bdfd3ff17bc6fed246c4c8f6b519d9ed08d0b3'; + +module.exports=config; \ No newline at end of file diff --git a/backend/express.js b/backend/express.js index 29e89b2..017d375 100644 --- a/backend/express.js +++ b/backend/express.js @@ -1,4 +1,7 @@ var amazonHelper = require('./helpers/amazon'); +var databaseHelper = require('./helpers/database'); + +const config = require('./config'); require('isomorphic-fetch'); @@ -7,11 +10,6 @@ var alexa = require('alexa-app'); var bodyParser = require('body-parser'); -const dbURL = 'mongodb://localhost:27017/tellall'; -const PORT = 5000; -const TOKEN = 'Atza|IwEBIMv0spn-eZhjP8-R2Jjkb4VUi-EY4V3MX-wlJyr2P6YBUmIChl7VKgRberEdQ-Wolp53SqfwHbxlSo4-rdUgFYFAxImB622boeqUBBCtwybP0OTBJBT1CVm_FBr48Li5LkwR9DxPciCnc052ddohpjGuGZxsCqIJo2c-7LPEhH0Lx63VOFgfPIfBsrVqjDbPNrr5ApPUijKYZWurktb6ytTks8eFUSTyv3FDbE5HEng0qpE4mgSjdgBkEc8BgUfpm2QGvctINH9SioUJOJonxgTPYhbD4BEd-jdQHsltKFzkb3-Rm3lFhEu3_CQFxCeQ6yGe1the_qID3vnPfpSY6hyblgF5L_5d7bE7BWg8fm6rSwXv67L2sMwBOji6cuR0wVVvXfYxmgGIMGkVQrq-SPSdtCpx2BvBz2SqqN6a_98svP8ukvhwc_oJsN6VwEmTDFgutNf-XuGkuVii-k9-DncwuwD00LvJG1FhBvbvSyuv-a3LAJSqTmroemTDG0xzLn7ULFY5p-93sM0_ZNGFeW-lL_r1ldqM_5lFRKDta1Tkg7lT9-rHftgnpGs4zv7vGLIPzHpNiXjsKyCk-wMQrihhWlR15kiz7oKDeTf6wCIETg'; -const USER_ID = 'amzn1.ask.account.AGE34MG3VIQ75D4Q5CXFK25GUOXHZ2MIVSII3QUCOT3CP44IPE25PLD3DWW7HLWO2KVC7PC7VUXSZZEFPVHJ6PTDYVYYESJE35CE6VEXCE3BI3AK24TGO4CJXFF3SZ7IA4QVJRZC6EN4MUF2WUP4IB4CGDLRZZMYQENOFNBYWFXZHMZ5PA6S6ERK7NGEPUSDHXWKH26UGMIATMI'; - var MongoClient = require ('mongodb').MongoClient; var ObjectID = require ('mongodb').ObjectID; @@ -55,40 +53,33 @@ router.get ('/deleteSkill/:skillID', async (req, res, next) => { }); router.post ('/updateSkill/:id', async (req, res, next) => { - try { + let id = req.params.id; let skill = req.body; delete skill._id; if (id !== '-1'){ - let completeResult = {databaseUpdate:false, amazonUpdate:false, amazonMessage:''}; - - let result = db.collection('skill_list').update({_id: ObjectID(id)}, skill,{upsert:true}, (err, result)=>{ - if (err) throw(err); - - completeResult.databaseUpdate = (JSON.parse(result).nModified===1); - - let generatedInteractionModel = amazonHelper.generateInteractionModel(skill); - - fetch(`https://api.amazonalexa.com/v0/skills/${skill.skillID}/interactionModel/locales/en-US`, { - method: 'POST', - headers: { - Authorization: TOKEN - }, - body: generatedInteractionModel - }).then(l=>l.text()).then(result=>{ - completeResult.amazonUpdate = (JSON.stringify(result)===JSON.stringify({})); - completeResult.amazonMessage = JSON.stringify(result); - res.json(completeResult); - }); + amazonHelper.updateSkill(skill,db).then(amazonResult=>{ + if (amazonResult===200 || amazonResult===202){ + //Skill uploaded, it's ok to update database + let result = db.collection('skill_list').update({_id: ObjectID(id)}, skill,{upsert:true}, (err, result)=>{ + if (JSON.parse(result).nModified===1){ + //database update ok + res.json({result:0, message:'ok'}); + }else{ + res.json({result:-1, message:'ok'}); + } + }); + }else{ + res.json({result:-1, message:'Amazon result ' + amazonResult}); + } + }).catch(e=>{ + //skill upload went wrong, don't update database, send error + res.json({result:-1, message:e}); }); + }else{ //no new skills for now } - - } catch (e) { - console.log ('error:', e); - next (e); - } }); var skillID = 'amzn1.ask.skill.7115bfc9-313e-4728-830b-ebd19ce96cb3'; @@ -168,10 +159,16 @@ app.use (bodyParser.json ()); app.use ('/', router); -MongoClient.connect (dbURL).then (database => { +MongoClient.connect (config.dbURL).then (database => { db = database; db.collection ('intent_list'); - app.listen (PORT, () => - console.log ('Express server running on port ' + PORT) - ); + + databaseHelper.initModule(db); + + app.listen (config.PORT, () =>{ + console.log ('Express server running on port ' + config.PORT); + databaseHelper.loadTokens(); + }); +}).catch(e=>{ + console.log("error : " + e); }); \ No newline at end of file diff --git a/backend/helpers/amazon.js b/backend/helpers/amazon.js index 2ebcf9f..2bca863 100644 --- a/backend/helpers/amazon.js +++ b/backend/helpers/amazon.js @@ -1,6 +1,7 @@ require('isomorphic-fetch'); - -var ObjectID = require ('mongodb').ObjectID; +const config = require('../config'); +var request = require("request"); +var token = require('./token'); var getBuildStatus = function(skillID){ @@ -18,45 +19,102 @@ var getBuildStatus = function(skillID){ } } -module.exports = { - generateInteractionModel: function(skill){ - try{ - let result = {}; - let allIntents = []; - let defaultIntents = [{ - name: "AMAZON.CancelIntent", - samples: [] - }, - { - name: "AMAZON.HelpIntent", - samples: [] - }, - { - name: "AMAZON.StopIntent", - samples: [] - }]; - - /* - defaultIntents.map(intent=>{ - allIntents.push(intent); - }); - */ - - skill.intents.map(intent=>{ - allIntents.push({name: intent.intentName, samples: intent.questions}); - }); - - result.interactionModel = {}; - - result.interactionModel.languageModel = { - invocationName: skill.invocationName, - intents: allIntents - }; - - return JSON.stringify(result); - }catch(e){ - console.log("error generate : " + e); - } - } +var refreshToken = function(db){ + return new Promise((resolve, reject)=>{ + var options = { method: 'POST', + url: 'https://api.amazon.com/auth/o2/token', + headers: + { 'cache-control': 'no-cache', + 'content-type': 'application/x-www-form-urlencoded' }, + form: + { grant_type: 'refresh_token', + refresh_token: config.REFRESH_TOKEN, + client_id: config.CLIENT_ID, + client_secret: config.CLIENT_SECRET } }; + + request(options, function (error, response, body) { + if (error) reject(error); + parsedResponse = JSON.parse(body); + if (parsedResponse.refresh_token){ + try{ + token.updateTokens(parsedResponse.refresh_token, parsedResponse.access_token, parsedResponse.expires_in,db); + resolve(); + }catch(e){ + reject(e); + } + } + }); + }); +} +var generateInteractionModel = function(skill){ + let result = {}; + let allIntents = []; + let defaultIntents = [{ + name: "AMAZON.CancelIntent", + samples: [] + }, + { + name: "AMAZON.HelpIntent", + samples: [] + }, + { + name: "AMAZON.StopIntent", + samples: [] + }]; + + /* + defaultIntents.map(intent=>{ + allIntents.push(intent); + }); + */ + + skill.intents.map(intent=>{ + allIntents.push({name: intent.intentName, samples: intent.questions}); + }); + + result.interactionModel = {}; + + result.interactionModel.languageModel = { + invocationName: skill.invocationName, + intents: allIntents + }; + + return JSON.stringify(result); +} + +var uploadSkill = function(skill){ + return fetch(`https://api.amazonalexa.com/v0/skills/${skill.skillID}/interactionModel/locales/en-US`, { + method: 'POST', + headers: { + Authorization: config.TOKEN + }, + body: generateInteractionModel(skill) + }); +} + +module.exports = { + updateSkill: function(skill, db){ + console.log("update skill function"); + + return new Promise((resolve,reject)=>{ + if (new Date() / 1000 > config.TOKEN_EXPIRES_IN){ + refreshToken(db).then(()=>{ + uploadSkill(skill).then(response=>{ + resolve(response.status); + }); + }).catch(e=>{ + reject(e); + }); + }else{ + uploadSkill(skill).then(response=>{ + resolve(response.status); + }).catch(e=>{ + reject(e); + }) + } + }); + + + } }; \ No newline at end of file diff --git a/backend/helpers/database.js b/backend/helpers/database.js new file mode 100644 index 0000000..ac580b8 --- /dev/null +++ b/backend/helpers/database.js @@ -0,0 +1,21 @@ +const config = require('../config'); + +var db = null; + + +module.exports = { + initModule : function(databaseObject){ + db=databaseObject; + }, + + loadTokens : function(){ + db.collection('token_list').findOne().then(tokens=>{ + config.TOKEN = tokens.access_token; + config.REFRESH_TOKEN = tokens.refresh_token; + config.TOKEN_EXPIRES_IN = tokens.expires_in; + console.log("Tokens loaded"); + }).catch(e=>{ + console.log("Error loading tokens"); + }) + } +} \ No newline at end of file diff --git a/backend/helpers/token.js b/backend/helpers/token.js new file mode 100644 index 0000000..bb95186 --- /dev/null +++ b/backend/helpers/token.js @@ -0,0 +1,19 @@ +const config = require('../config'); +var ObjectID = require ('mongodb').ObjectID; + +module.exports = { + updateTokens : function (refresh_token, access_token, expires_in, db){ + let newTokenDocument = { + id:1, + refresh_token:refresh_token, + access_token: access_token, + expires_in: (new Date() / 1000) + expires_in + } + let result = db.collection('token_list').update({id:1}, newTokenDocument,{upsert:true}, (err, result)=>{ + if (err) throw new Error(err); + config.REFRESH_TOKEN = refresh_token; + config.TOKEN = access_token; + config.TOKEN_EXPIRES_IN = newTokenDocument.expires_in; + }); + } +}; \ No newline at end of file diff --git a/backend/package.json b/backend/package.json index a496860..16afe98 100644 --- a/backend/package.json +++ b/backend/package.json @@ -8,7 +8,8 @@ "body-parser": "^1.13.1", "ejs": "^2.3.1", "express": "^4.13.0", - "isomorphic-fetch": "^2.2.1" + "isomorphic-fetch": "^2.2.1", + "request": "^2.83.0" }, "author": "Matt Kruse (http://mattkruse.com/)", "license": "MIT" diff --git a/web/src/App.js b/web/src/App.js index e2b5455..df1c76f 100644 --- a/web/src/App.js +++ b/web/src/App.js @@ -97,16 +97,14 @@ class App extends Component { handleSaveLaunchRequestClick(name, answer){ this.setState({invocationName: name, invocationAnswer: answer}); - try{ + console.log("handleSaveLaunchRequest"); updateSkill(this.createSkill(this.state.allIntents,name,answer)).then(l=>l.text()).then(result=>{ let jResult = JSON.parse(result); - if (!(jResult.databaseUpdate && jResult.amazonUpdate)){ - alert('Database update : ' + jResult.databaseUpdate + '\r\nAmazon update : ' + jResult.amazonUpdate + '\r\nMessage : ' + jResult.amazonMessage); - } + console.log(jResult.result); + console.log(jResult.message); + }).catch(e=>{ + console.log("Error :" + e); }); - }catch(e){ - alert("exception"); - } } handleDeleteIntentClick(selectedIntent){ @@ -120,12 +118,13 @@ class App extends Component { this.setState({allIntents: newAllIntents, selectedIntent: {intentName:'', questions:[''],answer:''}}); updateSkill(this.createSkill(newAllIntents)).then(l=>l.text()).then(result=>{ let jResult = JSON.parse(result); - if (!(jResult.databaseUpdate && jResult.amazonUpdate)){ - alert('Database update : ' + jResult.databaseUpdate + '\r\nAmazon update : ' + jResult.amazonUpdate + '\r\nMessage : ' + jResult.amazonMessage); - } + console.log(jResult.result); + console.log(jResult.message); + }).catch(e=>{ + console.log("error : " + e); }); }catch(e){ - alert("exception"); + console.log("error : " + e); } } } @@ -141,16 +140,15 @@ class App extends Component { newAllIntents[this.state.selectedIndex] = selectedIntent; this.setState({allIntents: newAllIntents, selectedIntent: selectedIntent}); } - try{ - updateSkill(this.createSkill(newAllIntents)).then(l=>l.text()).then(result=>{ - let jResult = JSON.parse(result); - if (!(jResult.databaseUpdate && jResult.amazonUpdate)){ - alert('Database update : ' + jResult.databaseUpdate + '\r\nAmazon update : ' + jResult.amazonUpdate + '\r\nMessage : ' + jResult.amazonMessage); - } - }); - }catch(e){ - alert("exception"); - } + + updateSkill(this.createSkill(newAllIntents)).then(l=>l.text()).then(result=>{ + let jResult = JSON.parse(result); + console.log(jResult.result); + console.log(jResult.message); + }).catch(e=>{ + console.log("error : " + e); + }); + } handleAddIntentClick(){ From b55dd56859acc2d28353ecf534cb361f4c950b77 Mon Sep 17 00:00:00 2001 From: GotPPay Date: Fri, 5 Jan 2018 01:00:10 +0100 Subject: [PATCH 08/83] readme update --- web/README.md | 2239 +------------------------------------------------ 1 file changed, 36 insertions(+), 2203 deletions(-) diff --git a/web/README.md b/web/README.md index b8c0b74..ab85cb3 100644 --- a/web/README.md +++ b/web/README.md @@ -1,2229 +1,62 @@ -This project was bootstrapped with [Create React App](https://github.com/facebookincubator/create-react-app). +To obtain new Auth Code : -Below you will find some information on how to perform common tasks.
-You can find the most recent version of this guide [here](https://github.com/facebookincubator/create-react-app/blob/master/packages/react-scripts/template/README.md). +https://www.amazon.com/ap/oa?client_id=amzn1.application-oa2-client.c748ca56ded04a95b236979898585ff7&scope=alexa::ask:skills:readwrite alexa::ask:models:readwrite alexa::ask:skills:test&response_type=code&redirect_uri=https://layla.amazon.com/api/skill/link/M2ODJY6EXOY6KO -## Table of Contents +Response URL (Decoded) : -- [Updating to New Releases](#updating-to-new-releases) -- [Sending Feedback](#sending-feedback) -- [Folder Structure](#folder-structure) -- [Available Scripts](#available-scripts) - - [npm start](#npm-start) - - [npm test](#npm-test) - - [npm run build](#npm-run-build) - - [npm run eject](#npm-run-eject) -- [Supported Language Features and Polyfills](#supported-language-features-and-polyfills) -- [Syntax Highlighting in the Editor](#syntax-highlighting-in-the-editor) -- [Displaying Lint Output in the Editor](#displaying-lint-output-in-the-editor) -- [Debugging in the Editor](#debugging-in-the-editor) -- [Formatting Code Automatically](#formatting-code-automatically) -- [Changing the Page ``](#changing-the-page-title) -- [Installing a Dependency](#installing-a-dependency) -- [Importing a Component](#importing-a-component) -- [Code Splitting](#code-splitting) -- [Adding a Stylesheet](#adding-a-stylesheet) -- [Post-Processing CSS](#post-processing-css) -- [Adding a CSS Preprocessor (Sass, Less etc.)](#adding-a-css-preprocessor-sass-less-etc) -- [Adding Images, Fonts, and Files](#adding-images-fonts-and-files) -- [Using the `public` Folder](#using-the-public-folder) - - [Changing the HTML](#changing-the-html) - - [Adding Assets Outside of the Module System](#adding-assets-outside-of-the-module-system) - - [When to Use the `public` Folder](#when-to-use-the-public-folder) -- [Using Global Variables](#using-global-variables) -- [Adding Bootstrap](#adding-bootstrap) - - [Using a Custom Theme](#using-a-custom-theme) -- [Adding Flow](#adding-flow) -- [Adding Custom Environment Variables](#adding-custom-environment-variables) - - [Referencing Environment Variables in the HTML](#referencing-environment-variables-in-the-html) - - [Adding Temporary Environment Variables In Your Shell](#adding-temporary-environment-variables-in-your-shell) - - [Adding Development Environment Variables In `.env`](#adding-development-environment-variables-in-env) -- [Can I Use Decorators?](#can-i-use-decorators) -- [Integrating with an API Backend](#integrating-with-an-api-backend) - - [Node](#node) - - [Ruby on Rails](#ruby-on-rails) -- [Proxying API Requests in Development](#proxying-api-requests-in-development) - - ["Invalid Host Header" Errors After Configuring Proxy](#invalid-host-header-errors-after-configuring-proxy) - - [Configuring the Proxy Manually](#configuring-the-proxy-manually) - - [Configuring a WebSocket Proxy](#configuring-a-websocket-proxy) -- [Using HTTPS in Development](#using-https-in-development) -- [Generating Dynamic `<meta>` Tags on the Server](#generating-dynamic-meta-tags-on-the-server) -- [Pre-Rendering into Static HTML Files](#pre-rendering-into-static-html-files) -- [Injecting Data from the Server into the Page](#injecting-data-from-the-server-into-the-page) -- [Running Tests](#running-tests) - - [Filename Conventions](#filename-conventions) - - [Command Line Interface](#command-line-interface) - - [Version Control Integration](#version-control-integration) - - [Writing Tests](#writing-tests) - - [Testing Components](#testing-components) - - [Using Third Party Assertion Libraries](#using-third-party-assertion-libraries) - - [Initializing Test Environment](#initializing-test-environment) - - [Focusing and Excluding Tests](#focusing-and-excluding-tests) - - [Coverage Reporting](#coverage-reporting) - - [Continuous Integration](#continuous-integration) - - [Disabling jsdom](#disabling-jsdom) - - [Snapshot Testing](#snapshot-testing) - - [Editor Integration](#editor-integration) -- [Developing Components in Isolation](#developing-components-in-isolation) - - [Getting Started with Storybook](#getting-started-with-storybook) - - [Getting Started with Styleguidist](#getting-started-with-styleguidist) -- [Making a Progressive Web App](#making-a-progressive-web-app) - - [Opting Out of Caching](#opting-out-of-caching) - - [Offline-First Considerations](#offline-first-considerations) - - [Progressive Web App Metadata](#progressive-web-app-metadata) -- [Analyzing the Bundle Size](#analyzing-the-bundle-size) -- [Deployment](#deployment) - - [Static Server](#static-server) - - [Other Solutions](#other-solutions) - - [Serving Apps with Client-Side Routing](#serving-apps-with-client-side-routing) - - [Building for Relative Paths](#building-for-relative-paths) - - [Azure](#azure) - - [Firebase](#firebase) - - [GitHub Pages](#github-pages) - - [Heroku](#heroku) - - [Netlify](#netlify) - - [Now](#now) - - [S3 and CloudFront](#s3-and-cloudfront) - - [Surge](#surge) -- [Advanced Configuration](#advanced-configuration) -- [Troubleshooting](#troubleshooting) - - [`npm start` doesn’t detect changes](#npm-start-doesnt-detect-changes) - - [`npm test` hangs on macOS Sierra](#npm-test-hangs-on-macos-sierra) - - [`npm run build` exits too early](#npm-run-build-exits-too-early) - - [`npm run build` fails on Heroku](#npm-run-build-fails-on-heroku) - - [`npm run build` fails to minify](#npm-run-build-fails-to-minify) - - [Moment.js locales are missing](#momentjs-locales-are-missing) -- [Something Missing?](#something-missing) +https://layla.amazon.com/api/skill/link/M2ODJY6EXOY6KO?code=ANCgZUfEFdlRRkpSNFuA&scope=alexa::ask:skills:readwrite alexa::ask:models:readwrite alexa::ask:skills:test -## Updating to New Releases +Code : ANCgZUfEFdlRRkpSNFuA -Create React App is divided into two packages: +======================================= -* `create-react-app` is a global command-line utility that you use to create new projects. -* `react-scripts` is a development dependency in the generated projects (including this one). +Now to get Access Token : -You almost never need to update `create-react-app` itself: it delegates all the setup to `react-scripts`. +Send a POST request to https://api.amazon.com/auth/o2/token with the following parameters -When you run `create-react-app`, it always creates the project with the latest version of `react-scripts` so you’ll get all the new features and improvements in newly created apps automatically. + - HTTP Header Parameters -To update an existing project to a new version of `react-scripts`, [open the changelog](https://github.com/facebookincubator/create-react-app/blob/master/CHANGELOG.md), find the version you’re currently on (check `package.json` in this folder if you’re not sure), and apply the migration instructions for the newer versions. + Content-Type: application/x-www-form-urlencoded -In most cases bumping the `react-scripts` version in `package.json` and running `npm install` in this folder should be enough, but it’s good to consult the [changelog](https://github.com/facebookincubator/create-react-app/blob/master/CHANGELOG.md) for potential breaking changes. + - HTTP Body Parameters -We commit to keeping the breaking changes minimal so you can upgrade `react-scripts` painlessly. + grant_type: authorization_code + code: The authorization code that was returned in the response. + client_id: The product’s client ID. To access this information, navigate to Amazon’s Developer Console. After you’ve logged in, click Get Started > under Alexa Voice Service, then click Edit next to a registered product (or create a new one). From the left navigation select Security Profile. This page contains both Client ID and Client Secret for your product. + client_secret: The product’s client secret. To access this information, navigate to Amazon’s Developer Console. After you’ve logged in, click Get Started > under Alexa Voice Service, then click Edit next to a registered product (or create a new one). From the left navigation select Security Profile. This page contains both Client ID and Client Secret for your product. + redirect_uri: One of the return URIs that you added to your app’s security profile when signing up. -## Sending Feedback +Response : -We are always open to [your feedback](https://github.com/facebookincubator/create-react-app/issues). - -## Folder Structure - -After creation, your project should look like this: - -``` -my-app/ - README.md - node_modules/ - package.json - public/ - index.html - favicon.ico - src/ - App.css - App.js - App.test.js - index.css - index.js - logo.svg -``` - -For the project to build, **these files must exist with exact filenames**: - -* `public/index.html` is the page template; -* `src/index.js` is the JavaScript entry point. - -You can delete or rename the other files. - -You may create subdirectories inside `src`. For faster rebuilds, only files inside `src` are processed by Webpack.<br> -You need to **put any JS and CSS files inside `src`**, otherwise Webpack won’t see them. - -Only files inside `public` can be used from `public/index.html`.<br> -Read instructions below for using assets from JavaScript and HTML. - -You can, however, create more top-level directories.<br> -They will not be included in the production build so you can use them for things like documentation. - -## Available Scripts - -In the project directory, you can run: - -### `npm start` - -Runs the app in the development mode.<br> -Open [http://localhost:3000](http://localhost:3000) to view it in the browser. - -The page will reload if you make edits.<br> -You will also see any lint errors in the console. - -### `npm test` - -Launches the test runner in the interactive watch mode.<br> -See the section about [running tests](#running-tests) for more information. - -### `npm run build` - -Builds the app for production to the `build` folder.<br> -It correctly bundles React in production mode and optimizes the build for the best performance. - -The build is minified and the filenames include the hashes.<br> -Your app is ready to be deployed! - -See the section about [deployment](#deployment) for more information. - -### `npm run eject` - -**Note: this is a one-way operation. Once you `eject`, you can’t go back!** - -If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. - -Instead, it will copy all the configuration files and the transitive dependencies (Webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. - -You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. - -## Supported Language Features and Polyfills - -This project supports a superset of the latest JavaScript standard.<br> -In addition to [ES6](https://github.com/lukehoban/es6features) syntax features, it also supports: - -* [Exponentiation Operator](https://github.com/rwaldron/exponentiation-operator) (ES2016). -* [Async/await](https://github.com/tc39/ecmascript-asyncawait) (ES2017). -* [Object Rest/Spread Properties](https://github.com/sebmarkbage/ecmascript-rest-spread) (stage 3 proposal). -* [Dynamic import()](https://github.com/tc39/proposal-dynamic-import) (stage 3 proposal) -* [Class Fields and Static Properties](https://github.com/tc39/proposal-class-public-fields) (part of stage 3 proposal). -* [JSX](https://facebook.github.io/react/docs/introducing-jsx.html) and [Flow](https://flowtype.org/) syntax. - -Learn more about [different proposal stages](https://babeljs.io/docs/plugins/#presets-stage-x-experimental-presets-). - -While we recommend using experimental proposals with some caution, Facebook heavily uses these features in the product code, so we intend to provide [codemods](https://medium.com/@cpojer/effective-javascript-codemods-5a6686bb46fb) if any of these proposals change in the future. - -Note that **the project only includes a few ES6 [polyfills](https://en.wikipedia.org/wiki/Polyfill)**: - -* [`Object.assign()`](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Object/assign) via [`object-assign`](https://github.com/sindresorhus/object-assign). -* [`Promise`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise) via [`promise`](https://github.com/then/promise). -* [`fetch()`](https://developer.mozilla.org/en/docs/Web/API/Fetch_API) via [`whatwg-fetch`](https://github.com/github/fetch). - -If you use any other ES6+ features that need **runtime support** (such as `Array.from()` or `Symbol`), make sure you are including the appropriate polyfills manually, or that the browsers you are targeting already support them. - -## Syntax Highlighting in the Editor - -To configure the syntax highlighting in your favorite text editor, head to the [relevant Babel documentation page](https://babeljs.io/docs/editors) and follow the instructions. Some of the most popular editors are covered. - -## Displaying Lint Output in the Editor - ->Note: this feature is available with `react-scripts@0.2.0` and higher.<br> ->It also only works with npm 3 or higher. - -Some editors, including Sublime Text, Atom, and Visual Studio Code, provide plugins for ESLint. - -They are not required for linting. You should see the linter output right in your terminal as well as the browser console. However, if you prefer the lint results to appear right in your editor, there are some extra steps you can do. - -You would need to install an ESLint plugin for your editor first. Then, add a file called `.eslintrc` to the project root: - -```js { - "extends": "react-app" + "access_token": "Atza|IwEBIBe6gDqrrowEEav6N-_6s4NztYeP3oG8PGWmu8ZiZw6lbOh3wNla3TK6pY-VEpT1d8an-dVf_n3kXJzVFsNo_4xBfZyFHGoCTDTFjs3yBRul4PVdBOhwwiH3-sgRLcUofZbe2oE06GmTcbfYtaStfXpQI5dfpldfnsJg_CvhSA6AHb_snJT3F6lyXzbV076d_3cYUMJxFldJGnYcviNHHxjjmuQTD06hhGzCbAxxe9eBmkuopRsNfyedLT2UlKP_ublah9CUGA3AdIX_3Iuke82jMwGnNl9gv7pbaDNEjAbj7IQSl3B08uuREtJq-oTBOjALNXRvFxTJmQjZwXNf9eHC7fSHJDdEPdZQU0AcffRQObAyAkUuL6Jv39OHzhb3Q64-zzoyODqnJyLP5SQZ2JVF53Kc_cTBqjIc9pXljqe7yEVk6JDs7q1zKbBibx_AQm57TO79IzWyLBzBMlYL5HdTsqEfRzLeDw2tws-hGMgkx2HWfdbYnmf5Qb4SyIhzvmmdfPLg3MVKTxjIBu1rx0xf3n0PLZP1EO6jsJPoMRPg77Gm4oit5Zp6s37ek3A3Vxh-ntoASpkrkxGTG9kVtRNt", + "refresh_token": "Atzr|IwEBICA3kDhfSJxlwvnQp9AD1o115AC_KBbFd5GBg8oN_QHWn2or5xFQ09BruuK6a07tGHtTt_9q2Y21mnOMH4RDtYXTVG9ADgLE6mHWKZFxYVwt3kHMiUJdY5lJcsOtWLoblrS-bJ0BEXXK5nVDt_bSI5IB7NUf-9QVZxhovRH_ANSxdTjJT0_rMIAZY3WEj68FEap49q_pg72BhnxHVZD2TC3zvX96_DN65HE5SoSgT7OiMAeiJewB_SyemW_HxBwaB-_X-G1ifOtnrzZ4gXTpOrEUlHI2YPuvRMBMtmz1h-nXDZYv3vwU3RA57Qj_ZNVcScj8_RXf2xq8w48v0PzZFXYBSalfnqPq6eUvSSbAJUp6bB8y596JlvR5dFQe_Z--X0Gkfo85IcyrI9D44vK9sJhrGhCVi2FDDa8pHczmNSen99JYZvDif-zpYzgbcymAkOV0gC7JvYMxlZfETT3NTBy7eVA7fJI1SZaeA_qW49xRcBkZBu5gkqTpeGWUU1cGr2aXRVVmXGM22NfV5E7KzvEBsCeHml_tCfxZeKY8Msd8hJb0Cd59u-_hsuc8oNjsOpIdFF976dY3uTmAgHWpG2PH", + "token_type": "bearer", + "expires_in": 3600 } -``` -Now your editor should report the linting warnings. +================================ -Note that even if you edit your `.eslintrc` file further, these changes will **only affect the editor integration**. They won’t affect the terminal and in-browser lint output. This is because Create React App intentionally provides a minimal set of rules that find common mistakes. +Now to get new Access token using refresh token : -If you want to enforce a coding style for your project, consider using [Prettier](https://github.com/jlongster/prettier) instead of ESLint style rules. +Send a POST request to https://api.amazon.com/auth/o2/token with the following parameters: -## Debugging in the Editor + - HTTP Header Parameters -**This feature is currently only supported by [Visual Studio Code](https://code.visualstudio.com) and [WebStorm](https://www.jetbrains.com/webstorm/).** + Content-Type: application/x-www-form-urlencoded -Visual Studio Code and WebStorm support debugging out of the box with Create React App. This enables you as a developer to write and debug your React code without leaving the editor, and most importantly it enables you to have a continuous development workflow, where context switching is minimal, as you don’t have to switch between tools. + - HTTP Body Parameters -### Visual Studio Code + grant_type: refresh_token + refresh_token: The URL-encoded refresh token returned with the last request for a new access token. + client_id: To access this information, navigate to Amazon’s Developer Console. After you’ve logged in, click Get Started > under Alexa Voice Service, then click Edit next to a registered product (or create a new one). From the left navigation select Security Profile. This page contains both Client ID and Client Secret for your product. + client_secret: The website’s client secret. To access this information, navigate to Amazon’s Developer Console. After you’ve logged in, click Get Started > under Alexa Voice Service, then click Edit next to a registered product (or create a new one). From the left navigation select Security Profile. This page contains both Client ID and Client Secret for your product. -You would need to have the latest version of [VS Code](https://code.visualstudio.com) and VS Code [Chrome Debugger Extension](https://marketplace.visualstudio.com/items?itemName=msjsdiag.debugger-for-chrome) installed. +Response : -Then add the block below to your `launch.json` file and put it inside the `.vscode` folder in your app’s root directory. - -```json { - "version": "0.2.0", - "configurations": [{ - "name": "Chrome", - "type": "chrome", - "request": "launch", - "url": "http://localhost:3000", - "webRoot": "${workspaceRoot}/src", - "sourceMapPathOverrides": { - "webpack:///src/*": "${webRoot}/*" - } - }] + "access_token": "Atza|IwEBILtBe3hrHovrMx7Oivng-RB2EKzvCm_epXJE2HXPMQzXTFqK10Zrqt-Z8paeRoLQBqbLCmqWvcr5RTNgw9qjtfzOTsOrXC1VKqKmxpqHTrJyn2TLGsCzFjBDfADNjCyufWTf2ZlsSzjxW2GiqCHlwoPSd9pFrLavtRThrm1J-5KvnFrj-yD-tYTSwrgX5W5p2SrjQxoE3aP5b96z6p8GvCL9lM1pddafAxkHb22A3IzR-pYGmEijb4ksRuaIf4WCNwssWV6GBIB2oJA5CU-Dtd2mOZZ5-dYpSSeCHyGumTYecTxxMVSdiVjCqB8WT6AtvvutWFQQoldHjJmIwBsTZP-iQcl-UyajOZJ03GqRUym5Hp-49uByzVG-MfR_Z5qVmYjjsLQEOLCY9kPVnmRGnOTj6YPjrHXibd6P8TQOMh4VTcgFpg-afKKABP6EeDwok1t2ivuYh5OJju-B1A6gzhMi4vQJYKq107e0QMYBBhrf_OqCgMbfnQZ8j40qocVGID5YWv8uk5wKyI61LrbzrTltmzxzNemzqbSBzwAlfNS6GW-jVjg8svsi1lb_EVRbhyOoWJWX3mEd-5GDYyUcyInleiAR0aIHVP94pZxqdiCamA", + "refresh_token": "Atzr|IwEBICA3kDhfSJxlwvnQp9AD1o115AC_KBbFd5GBg8oN_QHWn2or5xFQ09BruuK6a07tGHtTt_9q2Y21mnOMH4RDtYXTVG9ADgLE6mHWKZFxYVwt3kHMiUJdY5lJcsOtWLoblrS-bJ0BEXXK5nVDt_bSI5IB7NUf-9QVZxhovRH_ANSxdTjJT0_rMIAZY3WEj68FEap49q_pg72BhnxHVZD2TC3zvX96_DN65HE5SoSgT7OiMAeiJewB_SyemW_HxBwaB-_X-G1ifOtnrzZ4gXTpOrEUlHI2YPuvRMBMtmz1h-nXDZYv3vwU3RA57Qj_ZNVcScj8_RXf2xq8w48v0PzZFXYBSalfnqPq6eUvSSbAJUp6bB8y596JlvR5dFQe_Z--X0Gkfo85IcyrI9D44vK9sJhrGhCVi2FDDa8pHczmNSen99JYZvDif-zpYzgbcymAkOV0gC7JvYMxlZfETT3NTBy7eVA7fJI1SZaeA_qW49xRcBkZBu5gkqTpeGWUU1cGr2aXRVVmXGM22NfV5E7KzvEBsCeHml_tCfxZeKY8Msd8hJb0Cd59u-_hsuc8oNjsOpIdFF976dY3uTmAgHWpG2PH", + "token_type": "bearer", + "expires_in": 3600 } -``` ->Note: the URL may be different if you've made adjustments via the [HOST or PORT environment variables](#advanced-configuration). - -Start your app by running `npm start`, and start debugging in VS Code by pressing `F5` or by clicking the green debug icon. You can now write code, set breakpoints, make changes to the code, and debug your newly modified code—all from your editor. - -Having problems with VS Code Debugging? Please see their [troubleshooting guide](https://github.com/Microsoft/vscode-chrome-debug/blob/master/README.md#troubleshooting). - -### WebStorm - -You would need to have [WebStorm](https://www.jetbrains.com/webstorm/) and [JetBrains IDE Support](https://chrome.google.com/webstore/detail/jetbrains-ide-support/hmhgeddbohgjknpmjagkdomcpobmllji) Chrome extension installed. - -In the WebStorm menu `Run` select `Edit Configurations...`. Then click `+` and select `JavaScript Debug`. Paste `http://localhost:3000` into the URL field and save the configuration. - ->Note: the URL may be different if you've made adjustments via the [HOST or PORT environment variables](#advanced-configuration). - -Start your app by running `npm start`, then press `^D` on macOS or `F9` on Windows and Linux or click the green debug icon to start debugging in WebStorm. - -The same way you can debug your application in IntelliJ IDEA Ultimate, PhpStorm, PyCharm Pro, and RubyMine. - -## Formatting Code Automatically - -Prettier is an opinionated code formatter with support for JavaScript, CSS and JSON. With Prettier you can format the code you write automatically to ensure a code style within your project. See the [Prettier's GitHub page](https://github.com/prettier/prettier) for more information, and look at this [page to see it in action](https://prettier.github.io/prettier/). - -To format our code whenever we make a commit in git, we need to install the following dependencies: - -```sh -npm install --save husky lint-staged prettier -``` - -Alternatively you may use `yarn`: - -```sh -yarn add husky lint-staged prettier -``` - -* `husky` makes it easy to use githooks as if they are npm scripts. -* `lint-staged` allows us to run scripts on staged files in git. See this [blog post about lint-staged to learn more about it](https://medium.com/@okonetchnikov/make-linting-great-again-f3890e1ad6b8). -* `prettier` is the JavaScript formatter we will run before commits. - -Now we can make sure every file is formatted correctly by adding a few lines to the `package.json` in the project root. - -Add the following line to `scripts` section: - -```diff - "scripts": { -+ "precommit": "lint-staged", - "start": "react-scripts start", - "build": "react-scripts build", -``` - -Next we add a 'lint-staged' field to the `package.json`, for example: - -```diff - "dependencies": { - // ... - }, -+ "lint-staged": { -+ "src/**/*.{js,jsx,json,css}": [ -+ "prettier --single-quote --write", -+ "git add" -+ ] -+ }, - "scripts": { -``` - -Now, whenever you make a commit, Prettier will format the changed files automatically. You can also run `./node_modules/.bin/prettier --single-quote --write "src/**/*.{js,jsx}"` to format your entire project for the first time. - -Next you might want to integrate Prettier in your favorite editor. Read the section on [Editor Integration](https://github.com/prettier/prettier#editor-integration) on the Prettier GitHub page. - -## Changing the Page `<title>` - -You can find the source HTML file in the `public` folder of the generated project. You may edit the `<title>` tag in it to change the title from “React App” to anything else. - -Note that normally you wouldn’t edit files in the `public` folder very often. For example, [adding a stylesheet](#adding-a-stylesheet) is done without touching the HTML. - -If you need to dynamically update the page title based on the content, you can use the browser [`document.title`](https://developer.mozilla.org/en-US/docs/Web/API/Document/title) API. For more complex scenarios when you want to change the title from React components, you can use [React Helmet](https://github.com/nfl/react-helmet), a third party library. - -If you use a custom server for your app in production and want to modify the title before it gets sent to the browser, you can follow advice in [this section](#generating-dynamic-meta-tags-on-the-server). Alternatively, you can pre-build each page as a static HTML file which then loads the JavaScript bundle, which is covered [here](#pre-rendering-into-static-html-files). - -## Installing a Dependency - -The generated project includes React and ReactDOM as dependencies. It also includes a set of scripts used by Create React App as a development dependency. You may install other dependencies (for example, React Router) with `npm`: - -```sh -npm install --save react-router -``` - -Alternatively you may use `yarn`: - -```sh -yarn add react-router -``` - -This works for any library, not just `react-router`. - -## Importing a Component - -This project setup supports ES6 modules thanks to Babel.<br> -While you can still use `require()` and `module.exports`, we encourage you to use [`import` and `export`](http://exploringjs.com/es6/ch_modules.html) instead. - -For example: - -### `Button.js` - -```js -import React, { Component } from 'react'; - -class Button extends Component { - render() { - // ... - } -} - -export default Button; // Don’t forget to use export default! -``` - -### `DangerButton.js` - - -```js -import React, { Component } from 'react'; -import Button from './Button'; // Import a component from another file - -class DangerButton extends Component { - render() { - return <Button color="red" />; - } -} - -export default DangerButton; -``` - -Be aware of the [difference between default and named exports](http://stackoverflow.com/questions/36795819/react-native-es-6-when-should-i-use-curly-braces-for-import/36796281#36796281). It is a common source of mistakes. - -We suggest that you stick to using default imports and exports when a module only exports a single thing (for example, a component). That’s what you get when you use `export default Button` and `import Button from './Button'`. - -Named exports are useful for utility modules that export several functions. A module may have at most one default export and as many named exports as you like. - -Learn more about ES6 modules: - -* [When to use the curly braces?](http://stackoverflow.com/questions/36795819/react-native-es-6-when-should-i-use-curly-braces-for-import/36796281#36796281) -* [Exploring ES6: Modules](http://exploringjs.com/es6/ch_modules.html) -* [Understanding ES6: Modules](https://leanpub.com/understandinges6/read#leanpub-auto-encapsulating-code-with-modules) - -## Code Splitting - -Instead of downloading the entire app before users can use it, code splitting allows you to split your code into small chunks which you can then load on demand. - -This project setup supports code splitting via [dynamic `import()`](http://2ality.com/2017/01/import-operator.html#loading-code-on-demand). Its [proposal](https://github.com/tc39/proposal-dynamic-import) is in stage 3. The `import()` function-like form takes the module name as an argument and returns a [`Promise`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise) which always resolves to the namespace object of the module. - -Here is an example: - -### `moduleA.js` - -```js -const moduleA = 'Hello'; - -export { moduleA }; -``` -### `App.js` - -```js -import React, { Component } from 'react'; - -class App extends Component { - handleClick = () => { - import('./moduleA') - .then(({ moduleA }) => { - // Use moduleA - }) - .catch(err => { - // Handle failure - }); - }; - - render() { - return ( - <div> - <button onClick={this.handleClick}>Load</button> - </div> - ); - } -} - -export default App; -``` - -This will make `moduleA.js` and all its unique dependencies as a separate chunk that only loads after the user clicks the 'Load' button. - -You can also use it with `async` / `await` syntax if you prefer it. - -### With React Router - -If you are using React Router check out [this tutorial](http://serverless-stack.com/chapters/code-splitting-in-create-react-app.html) on how to use code splitting with it. You can find the companion GitHub repository [here](https://github.com/AnomalyInnovations/serverless-stack-demo-client/tree/code-splitting-in-create-react-app). - -## Adding a Stylesheet - -This project setup uses [Webpack](https://webpack.js.org/) for handling all assets. Webpack offers a custom way of “extending” the concept of `import` beyond JavaScript. To express that a JavaScript file depends on a CSS file, you need to **import the CSS from the JavaScript file**: - -### `Button.css` - -```css -.Button { - padding: 20px; -} -``` - -### `Button.js` - -```js -import React, { Component } from 'react'; -import './Button.css'; // Tell Webpack that Button.js uses these styles - -class Button extends Component { - render() { - // You can use them as regular CSS styles - return <div className="Button" />; - } -} -``` - -**This is not required for React** but many people find this feature convenient. You can read about the benefits of this approach [here](https://medium.com/seek-ui-engineering/block-element-modifying-your-javascript-components-d7f99fcab52b). However you should be aware that this makes your code less portable to other build tools and environments than Webpack. - -In development, expressing dependencies this way allows your styles to be reloaded on the fly as you edit them. In production, all CSS files will be concatenated into a single minified `.css` file in the build output. - -If you are concerned about using Webpack-specific semantics, you can put all your CSS right into `src/index.css`. It would still be imported from `src/index.js`, but you could always remove that import if you later migrate to a different build tool. - -## Post-Processing CSS - -This project setup minifies your CSS and adds vendor prefixes to it automatically through [Autoprefixer](https://github.com/postcss/autoprefixer) so you don’t need to worry about it. - -For example, this: - -```css -.App { - display: flex; - flex-direction: row; - align-items: center; -} -``` - -becomes this: - -```css -.App { - display: -webkit-box; - display: -ms-flexbox; - display: flex; - -webkit-box-orient: horizontal; - -webkit-box-direction: normal; - -ms-flex-direction: row; - flex-direction: row; - -webkit-box-align: center; - -ms-flex-align: center; - align-items: center; -} -``` - -If you need to disable autoprefixing for some reason, [follow this section](https://github.com/postcss/autoprefixer#disabling). - -## Adding a CSS Preprocessor (Sass, Less etc.) - -Generally, we recommend that you don’t reuse the same CSS classes across different components. For example, instead of using a `.Button` CSS class in `<AcceptButton>` and `<RejectButton>` components, we recommend creating a `<Button>` component with its own `.Button` styles, that both `<AcceptButton>` and `<RejectButton>` can render (but [not inherit](https://facebook.github.io/react/docs/composition-vs-inheritance.html)). - -Following this rule often makes CSS preprocessors less useful, as features like mixins and nesting are replaced by component composition. You can, however, integrate a CSS preprocessor if you find it valuable. In this walkthrough, we will be using Sass, but you can also use Less, or another alternative. - -First, let’s install the command-line interface for Sass: - -```sh -npm install --save node-sass-chokidar -``` - -Alternatively you may use `yarn`: - -```sh -yarn add node-sass-chokidar -``` - -Then in `package.json`, add the following lines to `scripts`: - -```diff - "scripts": { -+ "build-css": "node-sass-chokidar src/ -o src/", -+ "watch-css": "npm run build-css && node-sass-chokidar src/ -o src/ --watch --recursive", - "start": "react-scripts start", - "build": "react-scripts build", - "test": "react-scripts test --env=jsdom", -``` - ->Note: To use a different preprocessor, replace `build-css` and `watch-css` commands according to your preprocessor’s documentation. - -Now you can rename `src/App.css` to `src/App.scss` and run `npm run watch-css`. The watcher will find every Sass file in `src` subdirectories, and create a corresponding CSS file next to it, in our case overwriting `src/App.css`. Since `src/App.js` still imports `src/App.css`, the styles become a part of your application. You can now edit `src/App.scss`, and `src/App.css` will be regenerated. - -To share variables between Sass files, you can use Sass imports. For example, `src/App.scss` and other component style files could include `@import "./shared.scss";` with variable definitions. - -To enable importing files without using relative paths, you can add the `--include-path` option to the command in `package.json`. - -``` -"build-css": "node-sass-chokidar --include-path ./src --include-path ./node_modules src/ -o src/", -"watch-css": "npm run build-css && node-sass-chokidar --include-path ./src --include-path ./node_modules src/ -o src/ --watch --recursive", -``` - -This will allow you to do imports like - -```scss -@import 'styles/_colors.scss'; // assuming a styles directory under src/ -@import 'nprogress/nprogress'; // importing a css file from the nprogress node module -``` - -At this point you might want to remove all CSS files from the source control, and add `src/**/*.css` to your `.gitignore` file. It is generally a good practice to keep the build products outside of the source control. - -As a final step, you may find it convenient to run `watch-css` automatically with `npm start`, and run `build-css` as a part of `npm run build`. You can use the `&&` operator to execute two scripts sequentially. However, there is no cross-platform way to run two scripts in parallel, so we will install a package for this: - -```sh -npm install --save npm-run-all -``` - -Alternatively you may use `yarn`: - -```sh -yarn add npm-run-all -``` - -Then we can change `start` and `build` scripts to include the CSS preprocessor commands: - -```diff - "scripts": { - "build-css": "node-sass-chokidar src/ -o src/", - "watch-css": "npm run build-css && node-sass-chokidar src/ -o src/ --watch --recursive", -- "start": "react-scripts start", -- "build": "react-scripts build", -+ "start-js": "react-scripts start", -+ "start": "npm-run-all -p watch-css start-js", -+ "build-js": "react-scripts build", -+ "build": "npm-run-all build-css build-js", - "test": "react-scripts test --env=jsdom", - "eject": "react-scripts eject" - } -``` - -Now running `npm start` and `npm run build` also builds Sass files. - -**Why `node-sass-chokidar`?** - -`node-sass` has been reported as having the following issues: - -- `node-sass --watch` has been reported to have *performance issues* in certain conditions when used in a virtual machine or with docker. - -- Infinite styles compiling [#1939](https://github.com/facebookincubator/create-react-app/issues/1939) - -- `node-sass` has been reported as having issues with detecting new files in a directory [#1891](https://github.com/sass/node-sass/issues/1891) - - `node-sass-chokidar` is used here as it addresses these issues. - -## Adding Images, Fonts, and Files - -With Webpack, using static assets like images and fonts works similarly to CSS. - -You can **`import` a file right in a JavaScript module**. This tells Webpack to include that file in the bundle. Unlike CSS imports, importing a file gives you a string value. This value is the final path you can reference in your code, e.g. as the `src` attribute of an image or the `href` of a link to a PDF. - -To reduce the number of requests to the server, importing images that are less than 10,000 bytes returns a [data URI](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs) instead of a path. This applies to the following file extensions: bmp, gif, jpg, jpeg, and png. SVG files are excluded due to [#1153](https://github.com/facebookincubator/create-react-app/issues/1153). - -Here is an example: - -```js -import React from 'react'; -import logo from './logo.png'; // Tell Webpack this JS file uses this image - -console.log(logo); // /logo.84287d09.png - -function Header() { - // Import result is the URL of your image - return <img src={logo} alt="Logo" />; -} - -export default Header; -``` - -This ensures that when the project is built, Webpack will correctly move the images into the build folder, and provide us with correct paths. - -This works in CSS too: - -```css -.Logo { - background-image: url(./logo.png); -} -``` - -Webpack finds all relative module references in CSS (they start with `./`) and replaces them with the final paths from the compiled bundle. If you make a typo or accidentally delete an important file, you will see a compilation error, just like when you import a non-existent JavaScript module. The final filenames in the compiled bundle are generated by Webpack from content hashes. If the file content changes in the future, Webpack will give it a different name in production so you don’t need to worry about long-term caching of assets. - -Please be advised that this is also a custom feature of Webpack. - -**It is not required for React** but many people enjoy it (and React Native uses a similar mechanism for images).<br> -An alternative way of handling static assets is described in the next section. - -## Using the `public` Folder - ->Note: this feature is available with `react-scripts@0.5.0` and higher. - -### Changing the HTML - -The `public` folder contains the HTML file so you can tweak it, for example, to [set the page title](#changing-the-page-title). -The `<script>` tag with the compiled code will be added to it automatically during the build process. - -### Adding Assets Outside of the Module System - -You can also add other assets to the `public` folder. - -Note that we normally encourage you to `import` assets in JavaScript files instead. -For example, see the sections on [adding a stylesheet](#adding-a-stylesheet) and [adding images and fonts](#adding-images-fonts-and-files). -This mechanism provides a number of benefits: - -* Scripts and stylesheets get minified and bundled together to avoid extra network requests. -* Missing files cause compilation errors instead of 404 errors for your users. -* Result filenames include content hashes so you don’t need to worry about browsers caching their old versions. - -However there is an **escape hatch** that you can use to add an asset outside of the module system. - -If you put a file into the `public` folder, it will **not** be processed by Webpack. Instead it will be copied into the build folder untouched. To reference assets in the `public` folder, you need to use a special variable called `PUBLIC_URL`. - -Inside `index.html`, you can use it like this: - -```html -<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico"> -``` - -Only files inside the `public` folder will be accessible by `%PUBLIC_URL%` prefix. If you need to use a file from `src` or `node_modules`, you’ll have to copy it there to explicitly specify your intention to make this file a part of the build. - -When you run `npm run build`, Create React App will substitute `%PUBLIC_URL%` with a correct absolute path so your project works even if you use client-side routing or host it at a non-root URL. - -In JavaScript code, you can use `process.env.PUBLIC_URL` for similar purposes: - -```js -render() { - // Note: this is an escape hatch and should be used sparingly! - // Normally we recommend using `import` for getting asset URLs - // as described in “Adding Images and Fonts” above this section. - return <img src={process.env.PUBLIC_URL + '/img/logo.png'} />; -} -``` - -Keep in mind the downsides of this approach: - -* None of the files in `public` folder get post-processed or minified. -* Missing files will not be called at compilation time, and will cause 404 errors for your users. -* Result filenames won’t include content hashes so you’ll need to add query arguments or rename them every time they change. - -### When to Use the `public` Folder - -Normally we recommend importing [stylesheets](#adding-a-stylesheet), [images, and fonts](#adding-images-fonts-and-files) from JavaScript. -The `public` folder is useful as a workaround for a number of less common cases: - -* You need a file with a specific name in the build output, such as [`manifest.webmanifest`](https://developer.mozilla.org/en-US/docs/Web/Manifest). -* You have thousands of images and need to dynamically reference their paths. -* You want to include a small script like [`pace.js`](http://github.hubspot.com/pace/docs/welcome/) outside of the bundled code. -* Some library may be incompatible with Webpack and you have no other option but to include it as a `<script>` tag. - -Note that if you add a `<script>` that declares global variables, you also need to read the next section on using them. - -## Using Global Variables - -When you include a script in the HTML file that defines global variables and try to use one of these variables in the code, the linter will complain because it cannot see the definition of the variable. - -You can avoid this by reading the global variable explicitly from the `window` object, for example: - -```js -const $ = window.$; -``` - -This makes it obvious you are using a global variable intentionally rather than because of a typo. - -Alternatively, you can force the linter to ignore any line by adding `// eslint-disable-line` after it. - -## Adding Bootstrap - -You don’t have to use [React Bootstrap](https://react-bootstrap.github.io) together with React but it is a popular library for integrating Bootstrap with React apps. If you need it, you can integrate it with Create React App by following these steps: - -Install React Bootstrap and Bootstrap from npm. React Bootstrap does not include Bootstrap CSS so this needs to be installed as well: - -```sh -npm install --save react-bootstrap bootstrap@3 -``` - -Alternatively you may use `yarn`: - -```sh -yarn add react-bootstrap bootstrap@3 -``` - -Import Bootstrap CSS and optionally Bootstrap theme CSS in the beginning of your ```src/index.js``` file: - -```js -import 'bootstrap/dist/css/bootstrap.css'; -import 'bootstrap/dist/css/bootstrap-theme.css'; -// Put any other imports below so that CSS from your -// components takes precedence over default styles. -``` - -Import required React Bootstrap components within ```src/App.js``` file or your custom component files: - -```js -import { Navbar, Jumbotron, Button } from 'react-bootstrap'; -``` - -Now you are ready to use the imported React Bootstrap components within your component hierarchy defined in the render method. Here is an example [`App.js`](https://gist.githubusercontent.com/gaearon/85d8c067f6af1e56277c82d19fd4da7b/raw/6158dd991b67284e9fc8d70b9d973efe87659d72/App.js) redone using React Bootstrap. - -### Using a Custom Theme - -Sometimes you might need to tweak the visual styles of Bootstrap (or equivalent package).<br> -We suggest the following approach: - -* Create a new package that depends on the package you wish to customize, e.g. Bootstrap. -* Add the necessary build steps to tweak the theme, and publish your package on npm. -* Install your own theme npm package as a dependency of your app. - -Here is an example of adding a [customized Bootstrap](https://medium.com/@tacomanator/customizing-create-react-app-aa9ffb88165) that follows these steps. - -## Adding Flow - -Flow is a static type checker that helps you write code with fewer bugs. Check out this [introduction to using static types in JavaScript](https://medium.com/@preethikasireddy/why-use-static-types-in-javascript-part-1-8382da1e0adb) if you are new to this concept. - -Recent versions of [Flow](http://flowtype.org/) work with Create React App projects out of the box. - -To add Flow to a Create React App project, follow these steps: - -1. Run `npm install --save flow-bin` (or `yarn add flow-bin`). -2. Add `"flow": "flow"` to the `scripts` section of your `package.json`. -3. Run `npm run flow init` (or `yarn flow init`) to create a [`.flowconfig` file](https://flowtype.org/docs/advanced-configuration.html) in the root directory. -4. Add `// @flow` to any files you want to type check (for example, to `src/App.js`). - -Now you can run `npm run flow` (or `yarn flow`) to check the files for type errors. -You can optionally use an IDE like [Nuclide](https://nuclide.io/docs/languages/flow/) for a better integrated experience. -In the future we plan to integrate it into Create React App even more closely. - -To learn more about Flow, check out [its documentation](https://flowtype.org/). - -## Adding Custom Environment Variables - ->Note: this feature is available with `react-scripts@0.2.3` and higher. - -Your project can consume variables declared in your environment as if they were declared locally in your JS files. By -default you will have `NODE_ENV` defined for you, and any other environment variables starting with -`REACT_APP_`. - -**The environment variables are embedded during the build time**. Since Create React App produces a static HTML/CSS/JS bundle, it can’t possibly read them at runtime. To read them at runtime, you would need to load HTML into memory on the server and replace placeholders in runtime, just like [described here](#injecting-data-from-the-server-into-the-page). Alternatively you can rebuild the app on the server anytime you change them. - ->Note: You must create custom environment variables beginning with `REACT_APP_`. Any other variables except `NODE_ENV` will be ignored to avoid accidentally [exposing a private key on the machine that could have the same name](https://github.com/facebookincubator/create-react-app/issues/865#issuecomment-252199527). Changing any environment variables will require you to restart the development server if it is running. - -These environment variables will be defined for you on `process.env`. For example, having an environment -variable named `REACT_APP_SECRET_CODE` will be exposed in your JS as `process.env.REACT_APP_SECRET_CODE`. - -There is also a special built-in environment variable called `NODE_ENV`. You can read it from `process.env.NODE_ENV`. When you run `npm start`, it is always equal to `'development'`, when you run `npm test` it is always equal to `'test'`, and when you run `npm run build` to make a production bundle, it is always equal to `'production'`. **You cannot override `NODE_ENV` manually.** This prevents developers from accidentally deploying a slow development build to production. - -These environment variables can be useful for displaying information conditionally based on where the project is -deployed or consuming sensitive data that lives outside of version control. - -First, you need to have environment variables defined. For example, let’s say you wanted to consume a secret defined -in the environment inside a `<form>`: - -```jsx -render() { - return ( - <div> - <small>You are running this application in <b>{process.env.NODE_ENV}</b> mode.</small> - <form> - <input type="hidden" defaultValue={process.env.REACT_APP_SECRET_CODE} /> - </form> - </div> - ); -} -``` - -During the build, `process.env.REACT_APP_SECRET_CODE` will be replaced with the current value of the `REACT_APP_SECRET_CODE` environment variable. Remember that the `NODE_ENV` variable will be set for you automatically. - -When you load the app in the browser and inspect the `<input>`, you will see its value set to `abcdef`, and the bold text will show the environment provided when using `npm start`: - -```html -<div> - <small>You are running this application in <b>development</b> mode.</small> - <form> - <input type="hidden" value="abcdef" /> - </form> -</div> -``` - -The above form is looking for a variable called `REACT_APP_SECRET_CODE` from the environment. In order to consume this -value, we need to have it defined in the environment. This can be done using two ways: either in your shell or in -a `.env` file. Both of these ways are described in the next few sections. - -Having access to the `NODE_ENV` is also useful for performing actions conditionally: - -```js -if (process.env.NODE_ENV !== 'production') { - analytics.disable(); -} -``` - -When you compile the app with `npm run build`, the minification step will strip out this condition, and the resulting bundle will be smaller. - -### Referencing Environment Variables in the HTML - ->Note: this feature is available with `react-scripts@0.9.0` and higher. - -You can also access the environment variables starting with `REACT_APP_` in the `public/index.html`. For example: - -```html -<title>%REACT_APP_WEBSITE_NAME% -``` - -Note that the caveats from the above section apply: - -* Apart from a few built-in variables (`NODE_ENV` and `PUBLIC_URL`), variable names must start with `REACT_APP_` to work. -* The environment variables are injected at build time. If you need to inject them at runtime, [follow this approach instead](#generating-dynamic-meta-tags-on-the-server). - -### Adding Temporary Environment Variables In Your Shell - -Defining environment variables can vary between OSes. It’s also important to know that this manner is temporary for the -life of the shell session. - -#### Windows (cmd.exe) - -```cmd -set REACT_APP_SECRET_CODE=abcdef&&npm start -``` - -(Note: the lack of whitespace is intentional.) - -#### Linux, macOS (Bash) - -```bash -REACT_APP_SECRET_CODE=abcdef npm start -``` - -### Adding Development Environment Variables In `.env` - ->Note: this feature is available with `react-scripts@0.5.0` and higher. - -To define permanent environment variables, create a file called `.env` in the root of your project: - -``` -REACT_APP_SECRET_CODE=abcdef -``` - -`.env` files **should be** checked into source control (with the exclusion of `.env*.local`). - -#### What other `.env` files can be used? - ->Note: this feature is **available with `react-scripts@1.0.0` and higher**. - -* `.env`: Default. -* `.env.local`: Local overrides. **This file is loaded for all environments except test.** -* `.env.development`, `.env.test`, `.env.production`: Environment-specific settings. -* `.env.development.local`, `.env.test.local`, `.env.production.local`: Local overrides of environment-specific settings. - -Files on the left have more priority than files on the right: - -* `npm start`: `.env.development.local`, `.env.development`, `.env.local`, `.env` -* `npm run build`: `.env.production.local`, `.env.production`, `.env.local`, `.env` -* `npm test`: `.env.test.local`, `.env.test`, `.env` (note `.env.local` is missing) - -These variables will act as the defaults if the machine does not explicitly set them.
-Please refer to the [dotenv documentation](https://github.com/motdotla/dotenv) for more details. - ->Note: If you are defining environment variables for development, your CI and/or hosting platform will most likely need -these defined as well. Consult their documentation how to do this. For example, see the documentation for [Travis CI](https://docs.travis-ci.com/user/environment-variables/) or [Heroku](https://devcenter.heroku.com/articles/config-vars). - -## Can I Use Decorators? - -Many popular libraries use [decorators](https://medium.com/google-developers/exploring-es7-decorators-76ecb65fb841) in their documentation.
-Create React App doesn’t support decorator syntax at the moment because: - -* It is an experimental proposal and is subject to change. -* The current specification version is not officially supported by Babel. -* If the specification changes, we won’t be able to write a codemod because we don’t use them internally at Facebook. - -However in many cases you can rewrite decorator-based code without decorators just as fine.
-Please refer to these two threads for reference: - -* [#214](https://github.com/facebookincubator/create-react-app/issues/214) -* [#411](https://github.com/facebookincubator/create-react-app/issues/411) - -Create React App will add decorator support when the specification advances to a stable stage. - -## Integrating with an API Backend - -These tutorials will help you to integrate your app with an API backend running on another port, -using `fetch()` to access it. - -### Node -Check out [this tutorial](https://www.fullstackreact.com/articles/using-create-react-app-with-a-server/). -You can find the companion GitHub repository [here](https://github.com/fullstackreact/food-lookup-demo). - -### Ruby on Rails - -Check out [this tutorial](https://www.fullstackreact.com/articles/how-to-get-create-react-app-to-work-with-your-rails-api/). -You can find the companion GitHub repository [here](https://github.com/fullstackreact/food-lookup-demo-rails). - -## Proxying API Requests in Development - ->Note: this feature is available with `react-scripts@0.2.3` and higher. - -People often serve the front-end React app from the same host and port as their backend implementation.
-For example, a production setup might look like this after the app is deployed: - -``` -/ - static server returns index.html with React app -/todos - static server returns index.html with React app -/api/todos - server handles any /api/* requests using the backend implementation -``` - -Such setup is **not** required. However, if you **do** have a setup like this, it is convenient to write requests like `fetch('/api/todos')` without worrying about redirecting them to another host or port during development. - -To tell the development server to proxy any unknown requests to your API server in development, add a `proxy` field to your `package.json`, for example: - -```js - "proxy": "http://localhost:4000", -``` - -This way, when you `fetch('/api/todos')` in development, the development server will recognize that it’s not a static asset, and will proxy your request to `http://localhost:4000/api/todos` as a fallback. The development server will **only** attempt to send requests without `text/html` in its `Accept` header to the proxy. - -Conveniently, this avoids [CORS issues](http://stackoverflow.com/questions/21854516/understanding-ajax-cors-and-security-considerations) and error messages like this in development: - -``` -Fetch API cannot load http://localhost:4000/api/todos. No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://localhost:3000' is therefore not allowed access. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled. -``` - -Keep in mind that `proxy` only has effect in development (with `npm start`), and it is up to you to ensure that URLs like `/api/todos` point to the right thing in production. You don’t have to use the `/api` prefix. Any unrecognized request without a `text/html` accept header will be redirected to the specified `proxy`. - -The `proxy` option supports HTTP, HTTPS and WebSocket connections.
-If the `proxy` option is **not** flexible enough for you, alternatively you can: - -* [Configure the proxy yourself](#configuring-the-proxy-manually) -* Enable CORS on your server ([here’s how to do it for Express](http://enable-cors.org/server_expressjs.html)). -* Use [environment variables](#adding-custom-environment-variables) to inject the right server host and port into your app. - -### "Invalid Host Header" Errors After Configuring Proxy - -When you enable the `proxy` option, you opt into a more strict set of host checks. This is necessary because leaving the backend open to remote hosts makes your computer vulnerable to DNS rebinding attacks. The issue is explained in [this article](https://medium.com/webpack/webpack-dev-server-middleware-security-issues-1489d950874a) and [this issue](https://github.com/webpack/webpack-dev-server/issues/887). - -This shouldn’t affect you when developing on `localhost`, but if you develop remotely like [described here](https://github.com/facebookincubator/create-react-app/issues/2271), you will see this error in the browser after enabling the `proxy` option: - ->Invalid Host header - -To work around it, you can specify your public development host in a file called `.env.development` in the root of your project: - -``` -HOST=mypublicdevhost.com -``` - -If you restart the development server now and load the app from the specified host, it should work. - -If you are still having issues or if you’re using a more exotic environment like a cloud editor, you can bypass the host check completely by adding a line to `.env.development.local`. **Note that this is dangerous and exposes your machine to remote code execution from malicious websites:** - -``` -# NOTE: THIS IS DANGEROUS! -# It exposes your machine to attacks from the websites you visit. -DANGEROUSLY_DISABLE_HOST_CHECK=true -``` - -We don’t recommend this approach. - -### Configuring the Proxy Manually - ->Note: this feature is available with `react-scripts@1.0.0` and higher. - -If the `proxy` option is **not** flexible enough for you, you can specify an object in the following form (in `package.json`).
-You may also specify any configuration value [`http-proxy-middleware`](https://github.com/chimurai/http-proxy-middleware#options) or [`http-proxy`](https://github.com/nodejitsu/node-http-proxy#options) supports. -```js -{ - // ... - "proxy": { - "/api": { - "target": "", - "ws": true - // ... - } - } - // ... -} -``` - -All requests matching this path will be proxies, no exceptions. This includes requests for `text/html`, which the standard `proxy` option does not proxy. - -If you need to specify multiple proxies, you may do so by specifying additional entries. -Matches are regular expressions, so that you can use a regexp to match multiple paths. -```js -{ - // ... - "proxy": { - // Matches any request starting with /api - "/api": { - "target": "", - "ws": true - // ... - }, - // Matches any request starting with /foo - "/foo": { - "target": "", - "ssl": true, - "pathRewrite": { - "^/foo": "/foo/beta" - } - // ... - }, - // Matches /bar/abc.html but not /bar/sub/def.html - "/bar/[^/]*[.]html": { - "target": "", - // ... - }, - // Matches /baz/abc.html and /baz/sub/def.html - "/baz/.*/.*[.]html": { - "target": "" - // ... - } - } - // ... -} -``` - -### Configuring a WebSocket Proxy - -When setting up a WebSocket proxy, there are a some extra considerations to be aware of. - -If you’re using a WebSocket engine like [Socket.io](https://socket.io/), you must have a Socket.io server running that you can use as the proxy target. Socket.io will not work with a standard WebSocket server. Specifically, don't expect Socket.io to work with [the websocket.org echo test](http://websocket.org/echo.html). - -There’s some good documentation available for [setting up a Socket.io server](https://socket.io/docs/). - -Standard WebSockets **will** work with a standard WebSocket server as well as the websocket.org echo test. You can use libraries like [ws](https://github.com/websockets/ws) for the server, with [native WebSockets in the browser](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket). - -Either way, you can proxy WebSocket requests manually in `package.json`: - -```js -{ - // ... - "proxy": { - "/socket": { - // Your compatible WebSocket server - "target": "ws://", - // Tell http-proxy-middleware that this is a WebSocket proxy. - // Also allows you to proxy WebSocket requests without an additional HTTP request - // https://github.com/chimurai/http-proxy-middleware#external-websocket-upgrade - "ws": true - // ... - } - } - // ... -} -``` - -## Using HTTPS in Development - ->Note: this feature is available with `react-scripts@0.4.0` and higher. - -You may require the dev server to serve pages over HTTPS. One particular case where this could be useful is when using [the "proxy" feature](#proxying-api-requests-in-development) to proxy requests to an API server when that API server is itself serving HTTPS. - -To do this, set the `HTTPS` environment variable to `true`, then start the dev server as usual with `npm start`: - -#### Windows (cmd.exe) - -```cmd -set HTTPS=true&&npm start -``` - -(Note: the lack of whitespace is intentional.) - -#### Linux, macOS (Bash) - -```bash -HTTPS=true npm start -``` - -Note that the server will use a self-signed certificate, so your web browser will almost definitely display a warning upon accessing the page. - -## Generating Dynamic `` Tags on the Server - -Since Create React App doesn’t support server rendering, you might be wondering how to make `` tags dynamic and reflect the current URL. To solve this, we recommend to add placeholders into the HTML, like this: - -```html - - - - - -``` - -Then, on the server, regardless of the backend you use, you can read `index.html` into memory and replace `__OG_TITLE__`, `__OG_DESCRIPTION__`, and any other placeholders with values depending on the current URL. Just make sure to sanitize and escape the interpolated values so that they are safe to embed into HTML! - -If you use a Node server, you can even share the route matching logic between the client and the server. However duplicating it also works fine in simple cases. - -## Pre-Rendering into Static HTML Files - -If you’re hosting your `build` with a static hosting provider you can use [react-snapshot](https://www.npmjs.com/package/react-snapshot) or [react-snap](https://github.com/stereobooster/react-snap) to generate HTML pages for each route, or relative link, in your application. These pages will then seamlessly become active, or “hydrated”, when the JavaScript bundle has loaded. - -There are also opportunities to use this outside of static hosting, to take the pressure off the server when generating and caching routes. - -The primary benefit of pre-rendering is that you get the core content of each page _with_ the HTML payload—regardless of whether or not your JavaScript bundle successfully downloads. It also increases the likelihood that each route of your application will be picked up by search engines. - -You can read more about [zero-configuration pre-rendering (also called snapshotting) here](https://medium.com/superhighfives/an-almost-static-stack-6df0a2791319). - -## Injecting Data from the Server into the Page - -Similarly to the previous section, you can leave some placeholders in the HTML that inject global variables, for example: - -```js - - - - -``` - -Then, on the server, you can replace `__SERVER_DATA__` with a JSON of real data right before sending the response. The client code can then read `window.SERVER_DATA` to use it. **Make sure to [sanitize the JSON before sending it to the client](https://medium.com/node-security/the-most-common-xss-vulnerability-in-react-js-applications-2bdffbcc1fa0) as it makes your app vulnerable to XSS attacks.** - -## Running Tests - ->Note: this feature is available with `react-scripts@0.3.0` and higher.
->[Read the migration guide to learn how to enable it in older projects!](https://github.com/facebookincubator/create-react-app/blob/master/CHANGELOG.md#migrating-from-023-to-030) - -Create React App uses [Jest](https://facebook.github.io/jest/) as its test runner. To prepare for this integration, we did a [major revamp](https://facebook.github.io/jest/blog/2016/09/01/jest-15.html) of Jest so if you heard bad things about it years ago, give it another try. - -Jest is a Node-based runner. This means that the tests always run in a Node environment and not in a real browser. This lets us enable fast iteration speed and prevent flakiness. - -While Jest provides browser globals such as `window` thanks to [jsdom](https://github.com/tmpvar/jsdom), they are only approximations of the real browser behavior. Jest is intended to be used for unit tests of your logic and your components rather than the DOM quirks. - -We recommend that you use a separate tool for browser end-to-end tests if you need them. They are beyond the scope of Create React App. - -### Filename Conventions - -Jest will look for test files with any of the following popular naming conventions: - -* Files with `.js` suffix in `__tests__` folders. -* Files with `.test.js` suffix. -* Files with `.spec.js` suffix. - -The `.test.js` / `.spec.js` files (or the `__tests__` folders) can be located at any depth under the `src` top level folder. - -We recommend to put the test files (or `__tests__` folders) next to the code they are testing so that relative imports appear shorter. For example, if `App.test.js` and `App.js` are in the same folder, the test just needs to `import App from './App'` instead of a long relative path. Colocation also helps find tests more quickly in larger projects. - -### Command Line Interface - -When you run `npm test`, Jest will launch in the watch mode. Every time you save a file, it will re-run the tests, just like `npm start` recompiles the code. - -The watcher includes an interactive command-line interface with the ability to run all tests, or focus on a search pattern. It is designed this way so that you can keep it open and enjoy fast re-runs. You can learn the commands from the “Watch Usage” note that the watcher prints after every run: - -![Jest watch mode](http://facebook.github.io/jest/img/blog/15-watch.gif) - -### Version Control Integration - -By default, when you run `npm test`, Jest will only run the tests related to files changed since the last commit. This is an optimization designed to make your tests run fast regardless of how many tests you have. However it assumes that you don’t often commit the code that doesn’t pass the tests. - -Jest will always explicitly mention that it only ran tests related to the files changed since the last commit. You can also press `a` in the watch mode to force Jest to run all tests. - -Jest will always run all tests on a [continuous integration](#continuous-integration) server or if the project is not inside a Git or Mercurial repository. - -### Writing Tests - -To create tests, add `it()` (or `test()`) blocks with the name of the test and its code. You may optionally wrap them in `describe()` blocks for logical grouping but this is neither required nor recommended. - -Jest provides a built-in `expect()` global function for making assertions. A basic test could look like this: - -```js -import sum from './sum'; - -it('sums numbers', () => { - expect(sum(1, 2)).toEqual(3); - expect(sum(2, 2)).toEqual(4); -}); -``` - -All `expect()` matchers supported by Jest are [extensively documented here](https://facebook.github.io/jest/docs/en/expect.html#content).
-You can also use [`jest.fn()` and `expect(fn).toBeCalled()`](https://facebook.github.io/jest/docs/en/expect.html#tohavebeencalled) to create “spies” or mock functions. - -### Testing Components - -There is a broad spectrum of component testing techniques. They range from a “smoke test” verifying that a component renders without throwing, to shallow rendering and testing some of the output, to full rendering and testing component lifecycle and state changes. - -Different projects choose different testing tradeoffs based on how often components change, and how much logic they contain. If you haven’t decided on a testing strategy yet, we recommend that you start with creating simple smoke tests for your components: - -```js -import React from 'react'; -import ReactDOM from 'react-dom'; -import App from './App'; - -it('renders without crashing', () => { - const div = document.createElement('div'); - ReactDOM.render(, div); -}); -``` - -This test mounts a component and makes sure that it didn’t throw during rendering. Tests like this provide a lot value with very little effort so they are great as a starting point, and this is the test you will find in `src/App.test.js`. - -When you encounter bugs caused by changing components, you will gain a deeper insight into which parts of them are worth testing in your application. This might be a good time to introduce more specific tests asserting specific expected output or behavior. - -If you’d like to test components in isolation from the child components they render, we recommend using [`shallow()` rendering API](http://airbnb.io/enzyme/docs/api/shallow.html) from [Enzyme](http://airbnb.io/enzyme/). To install it, run: - -```sh -npm install --save enzyme enzyme-adapter-react-16 react-test-renderer -``` - -Alternatively you may use `yarn`: - -```sh -yarn add enzyme enzyme-adapter-react-16 react-test-renderer -``` - -As of Enzyme 3, you will need to install Enzyme along with an Adapter corresponding to the version of React you are using. (The examples above use the adapter for React 16.) - -The adapter will also need to be configured in your [global setup file](#initializing-test-environment): - -#### `src/setupTests.js` -```js -import { configure } from 'enzyme'; -import Adapter from 'enzyme-adapter-react-16'; - -configure({ adapter: new Adapter() }); -``` - -Now you can write a smoke test with it: - -```js -import React from 'react'; -import { shallow } from 'enzyme'; -import App from './App'; - -it('renders without crashing', () => { - shallow(); -}); -``` - -Unlike the previous smoke test using `ReactDOM.render()`, this test only renders `` and doesn’t go deeper. For example, even if `` itself renders a ` - - + + + +
); } diff --git a/web/src/components/IntentItem.js b/web/src/components/IntentItem.js index 435da7a..0aa29a5 100644 --- a/web/src/components/IntentItem.js +++ b/web/src/components/IntentItem.js @@ -21,9 +21,10 @@ class IntentItem extends Component {

diff --git a/web/src/components/IntentList.js b/web/src/components/IntentList.js index b97f990..53943c3 100644 --- a/web/src/components/IntentList.js +++ b/web/src/components/IntentList.js @@ -18,21 +18,25 @@ class IntentList extends Component { return (
+ onClick={this.props.onLaunchRequestClick} + disabled={this.props.waiting} >Launch request

Intents

{ this.state.intents.map((intent,index)=>{ return + onClick={this.state.onIntentClick} + waiting={this.props.waiting}> }) }

- +
); } diff --git a/web/src/components/LaunchRequest.js b/web/src/components/LaunchRequest.js index 28d5099..c713aa3 100644 --- a/web/src/components/LaunchRequest.js +++ b/web/src/components/LaunchRequest.js @@ -45,7 +45,9 @@ class LaunchRequest extends Component { value={this.state.invocationAnswer}/>



- + ); } diff --git a/web/src/config.js b/web/src/config.js index 180ca60..1a4de76 100644 --- a/web/src/config.js +++ b/web/src/config.js @@ -1,4 +1,4 @@ -export const BASE_URL = 'tellall.saburly.com'; +export const BASE_URL = 'localhost:5000'; export const INTENT_NAME_MAX_LENGTH = 30; export const QUESTION_MAX_LENGTH = 150; diff --git a/web/src/css/Intent.css b/web/src/css/Intent.css index 62b35e2..2556df7 100644 --- a/web/src/css/Intent.css +++ b/web/src/css/Intent.css @@ -1,8 +1,7 @@ /*IntentList and IntentItem CSS*/ .IntentList { width: 30%; - min-height: 9999px; - height: 9999px; + min-height: calc(100vh - 80px); float: left; background-color: #eff0f0; } @@ -49,8 +48,7 @@ .IntentDetails { float: left; width: 70%; - min-height: 9999px; - height: 9999px; + min-height: calc(100vh - 80px); background-color: #f5f5f5; } .QuestionBox { @@ -60,8 +58,7 @@ .LaunchRequestBox { float: left; width: 70%; - min-height: 9999px; - height: 9999px; + min-height: calc(100vh - 80px); background-color: #f5f5f5; } .ExplanationText { diff --git a/web/src/css/Intent.scss b/web/src/css/Intent.scss index b35497e..d755d26 100644 --- a/web/src/css/Intent.scss +++ b/web/src/css/Intent.scss @@ -1,9 +1,10 @@ +$minHeight : calc(100vh - 80px); + /*IntentList and IntentItem CSS*/ .IntentList{ width: 30%; - min-height:9999px; - height:9999px; + min-height:$minHeight; float:left; background-color: #eff0f0; } @@ -59,8 +60,7 @@ .IntentDetails{ float: left; width: 70%; - min-height:9999px; - height:9999px; + min-height:$minHeight; background-color: #f5f5f5; } @@ -73,8 +73,7 @@ .LaunchRequestBox{ float: left; width:70%; - min-height: 9999px; - height: 9999px; + min-height:$minHeight; background-color: #f5f5f5; } diff --git a/web/src/css/index.scss b/web/src/css/index.scss index 1055bc3..61a13a4 100644 --- a/web/src/css/index.scss +++ b/web/src/css/index.scss @@ -2,7 +2,6 @@ @include react-md-everything; - body { margin: 0; padding: 0; diff --git a/web/src/css/popup.scss b/web/src/css/popup.scss new file mode 100644 index 0000000..7f750bf --- /dev/null +++ b/web/src/css/popup.scss @@ -0,0 +1,174 @@ +.mm-popup { + display: none; } + +.mm-popup--visible { + display: block; } + +.mm-popup__overlay { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: 1000; + overflow: auto; + background: rgba(0, 0, 0, 0.1); } + +.mm-popup__close { + position: absolute; + top: 15px; + right: 20px; + padding: 0; + width: 20px; + height: 20px; + cursor: pointer; + outline: none; + text-align: center; + border-radius: 10px; + border: none; + text-indent: -9999px; + background: transparent url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACgAAAAoCAYAAACM/rhtAAAABGdBTUEAALGPC/xhBQAAB8BJREFUWAnFWAtsU1UY/s+5XTcYYxgfvERQeQXxNeYLjVFxLVvb2xasKIgSVNQoREVI1GhmfC6ioijiNDo1vBxb19uVtRWUzAQ1+EowOkSQzTBAUJio27r2Hr9TLJTaa7vK4yTtvec///+f7/znf5xzGf2PZnVMKRHUczEJNpgYDSEdPzTB6GdG1EbE2sxk+qqxsW5rrtNAT+/aZLtrkiDdLYhUIcSwQ9KsA7DaAbKdEWOCQBckxwrkOGP0Lf7rTAqrW+vzbT4kk91/1gAB7BqdYlVC0KUAsQuANOKKjwYUNYfff//PdNNZ3O4zqEe/FguZykhUYFGFQKspnBYGNW1LOplUWkaANtvUc3pY5FUAKwewb4jzR0KaN8ikoXrRZs2aVbBr3/6bddKfhHUHAugys+j3eCCwYv9/qflPgFab83ps52ookxZ6OOT3regtsNTJHY45fSO05yGh6wsFsZ1cIVtI035M5Uv0DQFabY77BWOLsNrmQrPi8Xq9vyaEjsXT4pg6VuiRABZfzAVzhwK+T9Lp5emIFru6QCd6CXv4+sRLSizHGpycM+yvayng/S6Do7QIJtZZVXVyOiz/sqDV4XAKweoxsDjUqM1PJ3QsaeVz5+bHtrc2IjWVmky8tKmhYVuy/qMsWOZyXSR0Wo4IDVxRWrIgmfF4vTctWdINF7oJljwQ7dG9lpkzC5PnOgywsrKSU1R/Gz6xo7hPwXT0scsnpkkXEnncjTw6kvZ3vJI8q5Lo5BUV3YaAuFthyjStof6HBP1EPbe3tOweNWpMF0AuGHveuNqtLS375NxxC8rQB7inkOd8wcaGDScKVOo8/fvmLwWOPZFIrDIxFgcYEbtnA9wgk1lZmBgwetrtnqGTbapqNG5Et06ZMhhuYzIal/Ta2tpOlMVnEAOeCqfzfEmLA0SV8KB+bljr9Wbc2ijrujpGwmdxOB+SCrJpckGiu+enT7/85uZM/P375FcjDn6LxsRMycsrPJ5B2PerOLE1mYTleNDvX8k4W4xK8HyZ3XlvJpkym+qJEa1B1VjHRwz7IBM/rBjBNodhxXLJy6N/dbvlSz4nr3xm08J+7QHkyTdI6EssDsftRjJWh2smtmwlyrZ29tBBbplSjHiT6ZyxIHZ1vHQnVBlRArTfaZq2J5kp0zuS+D2w5Hs4/FWj8sxI5bfa1TuF0GtAX4W0Na26uronlceon89FSI5FRPf1HJY4C2e1HUbMRnR5aCguyIf1RC143oW1piZ44Z/zdCFgYXpnYmnJrdg27HL2LW4sxg7A9YYhqthwEmJ99uJHOOXEiMxbNm76qkAX+kps9xSUyXHwzyps02tBv29urqcfGG4fzgKnIYrFMHTajkzbuzcAjBb3zb8ROtajTHqx2Cq8L4IL3JcruEMIxF4cck/niK4IjlV5vYN1NLeMPATDd6DKPBclhfmP5sipdxBSRdKCe/E7PScVEMJxnllszlfgcw/CYk8g4X8OSwbKHY7Lc9Up5aB2MNxvN2eC7UUnJ4DYXm51ON/AqXsuVvpAuFGrVAYUVUD991HBmuStL1eQ2N7hkG1DfqY92J4ze6vI4/EoCI53YcE7EBD3hAL+xVJH0/Llv5tFkRUTtOoiGrbY3ONz0F2MAOnPGG8FQLYRCi7DhP2yVTRnzpy8A391r8TipqNYzkZALEuWlRchpU9BGfbpF8Fi6yar6pjk8UzvBzt7SuM8grbwPBMPwArm37u6JmUSlOPyBLyjfVcdttGNPDfjQ7+/Jp1cU23tXp6fNwkRfTCmi/XydpiOLx0tRvoNWPzOoN+7iQe83u/h2Dvgh7Z0zKk0/afWF+C8VsYVTzigrUodT+6H6ut3IaKvw0KiEYp8pKpqUfJ4unfp16C7meD1Mk3JDprwovbdaLNNP+VQ3/hfKGwFJ+WasL+hwZjryEjY5/vZTObrYJFmznHJzNA+2/S1dI2BsLysUBBDw8qGdOr0Ixz75XCj/2FJOxlNpiyrQ/0CuZmF/b4Jhy2I2ie/qywFqHkAO/BkgJNzWu3OW7GTJZzT/EQV+meL5Veewudg0FhnjJacDIAul2sATlZPw3gavjR8nMBwGCDOofuA+m74o0de3BMMJ+KJwDD9GY2twdGtH+7GDybPeZTTbvthy+aRo8cUYxWPjhw1duO2rVu2JzMfr3dzYZF0LzdTmCvk832RPM9hCyaIEy+ZsBBpoRnlqyGXy1FCTzbPeKm0q1WoGnch1c0La9qHqXLxKE4lyqrS0YlKQVTBhJifKGOpfP+nXz5jRv9Yx8HliFwbXOtR1PFn0+lLC1Ayylrb0dn1IqJqHmr1alL4ApnT0inpLa1MVa9kungLQYk7B90SDGiakQ5DgAkBi02djeiqgrJC3A8WiQHFVUZfVBMyRs9yp3McrpPPIhHjXs02m0zspiafT54jDVtGgFJSpoDOqP4YfOU+KO+Cco1xsYaPGBHMdFOTRaBbl9+zyYlcWwZ17Vjw41dOmPAefDDj95+sACaWV+5ynQsLzMZ104NAGoVo/0Oe/eDgrVDUhtl2gl7IOA2Of/FnYgSAXRBPuoI+JS5WDzn11DdramqwyOxarwAmq7Ta3RfqIqZCwWhYZjicHbdDGhoHLeTXfmrHUWwngDaTWWkMe72/JMtn+/43YTIL+pAwwhkAAAAASUVORK5CYII=") no-repeat center center; + background-size: 100%; + margin: 0; } + +.mm-popup__input { + display: block; + width: 100%; + height: 30px; + border-radius: 3px; + background: #f5f5f5; + border: 1px solid #e9ebec; + outline: none; + -moz-box-sizing: border-box !important; + -webkit-box-sizing: border-box !important; + box-sizing: border-box !important; + font-size: 14px; + padding: 0 12px; + color: #808080; } + +.mm-popup__btn { + border-radius: 3px; + -moz-box-sizing: border-box; + -webkit-box-sizing: border-box; + box-sizing: border-box; + padding: 0 10px; + margin: 0; + line-height: 32px; + height: 32px; + border: 1px solid #666; + text-align: center; + display: inline-block; + font-size: 12px; + font-weight: 400; + color: #333; + background: transparent; + outline: none; + text-decoration: none; + cursor: pointer; + font-family: "Open Sans", sans-serif; } + +.mm-popup__btn--success { + background-color: #27ae60; + border-color: #27ae60; + color: #fff; } + +.mm-popup__btn--danger { + background-color: #c5545c; + border-color: #c5545c; + color: #fff; } + +.mm-popup__box { + width: 350px; + position: fixed; + top: 10%; + left: 50%; + margin-left: -175px; + background: #fff; + box-shadow: 0px 5px 20px 0px rgba(126, 137, 140, 0.2); + border-radius: 5px; + border: 1px solid #B8C8CC; + overflow: hidden; + z-index: 1001; } + +.mm-popup__box__header { + padding: 15px 20px; + background: #EDF5F7; + color: #454B4D; } + +.mm-popup__box__header__title { + margin: 0; + font-size: 16px; + text-align: left; + font-weight: 600; } + +.mm-popup__box__body { + padding: 20px; + line-height: 1.4; + font-size: 14px; + color: #454B4D; + background: #fff; + position: relative; + z-index: 2; } + +.mm-popup__box__body p { + margin: 0 0 5px; } + +.mm-popup__box__footer { + overflow: hidden; + padding: 40px 20px 20px; } + +.mm-popup__box__footer__right-space { + float: right; } + +.mm-popup__box__footer__right-space .mm-popup__btn { + margin-left: 5px; } + +.mm-popup__box__footer__left-space { + float: left; } + +.mm-popup__box__footer__left-space .mm-popup__btn { + margin-right: 5px; } + +.mm-popup__box--popover { + width: 300px; + margin-left: -150px; } + +.mm-popup__box--popover .mm-popup__close { + position: absolute; + top: 5px; + right: 5px; + padding: 0; + width: 20px; + height: 20px; + cursor: pointer; + outline: none; + text-align: center; + border-radius: 10px; + border: none; + text-indent: -9999px; + background: transparent url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACgAAAAoCAYAAACM/rhtAAAABGdBTUEAALGPC/xhBQAAB8BJREFUWAnFWAtsU1UY/s+5XTcYYxgfvERQeQXxNeYLjVFxLVvb2xasKIgSVNQoREVI1GhmfC6ioijiNDo1vBxb19uVtRWUzAQ1+EowOkSQzTBAUJio27r2Hr9TLJTaa7vK4yTtvec///+f7/znf5xzGf2PZnVMKRHUczEJNpgYDSEdPzTB6GdG1EbE2sxk+qqxsW5rrtNAT+/aZLtrkiDdLYhUIcSwQ9KsA7DaAbKdEWOCQBckxwrkOGP0Lf7rTAqrW+vzbT4kk91/1gAB7BqdYlVC0KUAsQuANOKKjwYUNYfff//PdNNZ3O4zqEe/FguZykhUYFGFQKspnBYGNW1LOplUWkaANtvUc3pY5FUAKwewb4jzR0KaN8ikoXrRZs2aVbBr3/6bddKfhHUHAugys+j3eCCwYv9/qflPgFab83ps52ookxZ6OOT3regtsNTJHY45fSO05yGh6wsFsZ1cIVtI035M5Uv0DQFabY77BWOLsNrmQrPi8Xq9vyaEjsXT4pg6VuiRABZfzAVzhwK+T9Lp5emIFru6QCd6CXv4+sRLSizHGpycM+yvayng/S6Do7QIJtZZVXVyOiz/sqDV4XAKweoxsDjUqM1PJ3QsaeVz5+bHtrc2IjWVmky8tKmhYVuy/qMsWOZyXSR0Wo4IDVxRWrIgmfF4vTctWdINF7oJljwQ7dG9lpkzC5PnOgywsrKSU1R/Gz6xo7hPwXT0scsnpkkXEnncjTw6kvZ3vJI8q5Lo5BUV3YaAuFthyjStof6HBP1EPbe3tOweNWpMF0AuGHveuNqtLS375NxxC8rQB7inkOd8wcaGDScKVOo8/fvmLwWOPZFIrDIxFgcYEbtnA9wgk1lZmBgwetrtnqGTbapqNG5Et06ZMhhuYzIal/Ta2tpOlMVnEAOeCqfzfEmLA0SV8KB+bljr9Wbc2ijrujpGwmdxOB+SCrJpckGiu+enT7/85uZM/P375FcjDn6LxsRMycsrPJ5B2PerOLE1mYTleNDvX8k4W4xK8HyZ3XlvJpkym+qJEa1B1VjHRwz7IBM/rBjBNodhxXLJy6N/dbvlSz4nr3xm08J+7QHkyTdI6EssDsftRjJWh2smtmwlyrZ29tBBbplSjHiT6ZyxIHZ1vHQnVBlRArTfaZq2J5kp0zuS+D2w5Hs4/FWj8sxI5bfa1TuF0GtAX4W0Na26uronlceon89FSI5FRPf1HJY4C2e1HUbMRnR5aCguyIf1RC143oW1piZ44Z/zdCFgYXpnYmnJrdg27HL2LW4sxg7A9YYhqthwEmJ99uJHOOXEiMxbNm76qkAX+kps9xSUyXHwzyps02tBv29urqcfGG4fzgKnIYrFMHTajkzbuzcAjBb3zb8ROtajTHqx2Cq8L4IL3JcruEMIxF4cck/niK4IjlV5vYN1NLeMPATDd6DKPBclhfmP5sipdxBSRdKCe/E7PScVEMJxnllszlfgcw/CYk8g4X8OSwbKHY7Lc9Up5aB2MNxvN2eC7UUnJ4DYXm51ON/AqXsuVvpAuFGrVAYUVUD991HBmuStL1eQ2N7hkG1DfqY92J4ze6vI4/EoCI53YcE7EBD3hAL+xVJH0/Llv5tFkRUTtOoiGrbY3ONz0F2MAOnPGG8FQLYRCi7DhP2yVTRnzpy8A391r8TipqNYzkZALEuWlRchpU9BGfbpF8Fi6yar6pjk8UzvBzt7SuM8grbwPBMPwArm37u6JmUSlOPyBLyjfVcdttGNPDfjQ7+/Jp1cU23tXp6fNwkRfTCmi/XydpiOLx0tRvoNWPzOoN+7iQe83u/h2Dvgh7Z0zKk0/afWF+C8VsYVTzigrUodT+6H6ut3IaKvw0KiEYp8pKpqUfJ4unfp16C7meD1Mk3JDprwovbdaLNNP+VQ3/hfKGwFJ+WasL+hwZjryEjY5/vZTObrYJFmznHJzNA+2/S1dI2BsLysUBBDw8qGdOr0Ixz75XCj/2FJOxlNpiyrQ/0CuZmF/b4Jhy2I2ie/qywFqHkAO/BkgJNzWu3OW7GTJZzT/EQV+meL5Veewudg0FhnjJacDIAul2sATlZPw3gavjR8nMBwGCDOofuA+m74o0de3BMMJ+KJwDD9GY2twdGtH+7GDybPeZTTbvthy+aRo8cUYxWPjhw1duO2rVu2JzMfr3dzYZF0LzdTmCvk832RPM9hCyaIEy+ZsBBpoRnlqyGXy1FCTzbPeKm0q1WoGnch1c0La9qHqXLxKE4lyqrS0YlKQVTBhJifKGOpfP+nXz5jRv9Yx8HliFwbXOtR1PFn0+lLC1Ayylrb0dn1IqJqHmr1alL4ApnT0inpLa1MVa9kungLQYk7B90SDGiakQ5DgAkBi02djeiqgrJC3A8WiQHFVUZfVBMyRs9yp3McrpPPIhHjXs02m0zspiafT54jDVtGgFJSpoDOqP4YfOU+KO+Cco1xsYaPGBHMdFOTRaBbl9+zyYlcWwZ17Vjw41dOmPAefDDj95+sACaWV+5ynQsLzMZ104NAGoVo/0Oe/eDgrVDUhtl2gl7IOA2Of/FnYgSAXRBPuoI+JS5WDzn11DdramqwyOxarwAmq7Ta3RfqIqZCwWhYZjicHbdDGhoHLeTXfmrHUWwngDaTWWkMe72/JMtn+/43YTIL+pAwwhkAAAAASUVORK5CYII=") no-repeat center center; + background-size: 100%; + margin: 0; + z-index: 3; } + +.mm-popup__box--popover .mm-popup__box__body { + padding: 20px; } + +@media (max-width: 420px) { + .mm-popup__box { + width: auto; + left: 10px; + right: 10px; + top: 10px; + margin-left: 0; } + .mm-popup__box__footer__left-space { + float: none; } + .mm-popup__box__footer__right-space { + float: none; } + .mm-popup__box__footer { + padding-top: 30px; } + .mm-popup__box__footer .mm-popup__btn { + display: block; + width: 100%; + text-align: center; + margin-top: 10px; } } From 7d79c03d1548bc5059d7a5be0ced02de116809e2 Mon Sep 17 00:00:00 2001 From: GotPPay Date: Thu, 11 Jan 2018 04:24:16 +0100 Subject: [PATCH 29/83] initial step 4 for online test --- backend/express.js | 129 ++++++++++++++++++++++++++--- backend/helpers/amazon.js | 41 ++++++++++ web/src/App.js | 134 ++++++++++++++++++------------- web/src/components/Contact.js | 48 +++++++++++ web/src/components/IntentList.js | 11 ++- web/src/config.js | 6 ++ web/src/css/Intent.css | 14 ++++ web/src/css/Intent.scss | 16 ++++ 8 files changed, 328 insertions(+), 71 deletions(-) create mode 100644 web/src/components/Contact.js diff --git a/backend/express.js b/backend/express.js index 4177ee6..3a32745 100644 --- a/backend/express.js +++ b/backend/express.js @@ -12,8 +12,19 @@ const router = express.Router (); var app = express (); +//User data for sending message, this is skill-related and will be in some skill container +var Name = null; +var Email = null; +var Message = null; +var State = 0; // states should be defined in seperate file. (Not sending message, Waiting for name, Waiting for email, Waiting for message) +//For now : +// 0 : Not sending Message +// 1 : Waiting for name +// 2 : Waiting for email +// 3 : Waiting for message + // ALWAYS setup the alexa app and attach it to express before anything else. -var alexaApp = new alexa.app ('step3'); // this means we still work with one skill +var alexaApp = new alexa.app ('saburly'); // this means we still work with one skill alexaApp.express ({ expressApp: app, @@ -39,6 +50,7 @@ var updateIntentsJSON = function () { .loadSkill (config.SKILL_DB_ID) .then (skill => { skill.intents.map (intent => { + alexaApp.intent ( intent.intentName, { @@ -50,9 +62,93 @@ var updateIntentsJSON = function () { } ); }); + alexaApp.launch ((request, response) => { return response.say (skill.invocationAnswer).shouldEndSession(false); }); + + alexaApp.intent('EmailIntentLaunch',{ + slots:[], + utterances: [ + 'I want to send a message', + 'I would like to send a message', + 'I would like to leave a message', + 'Leave a message' + ] + }, + function(request,response){ + Name = null; + Email = null; + Message = null; + State = 1; + + return response.say('Ok. What is your name').shouldEndSession(false); + } + ); + + //TODO : Watch out for this intent. It will make trouble with other regular intents + //if other intents have utterance with just one slot like {Data} + //It should be taken care somwhere before this, to check if Email Intent is invoked + //This is problem only if we introduce slot options for regular intents for users + alexaApp.intent('EmailIntent',{ + slots:{ + 'Name':'AMAZON.Person', + 'Email' : 'AMAZON.LITERAL', + 'Message': 'AMAZON.LITERAL', + 'Data': 'AMAZON.LITERAL' + }, + utterances: [ + 'My name is {Name}', + 'I am {Name}', + '{Data}', + 'My email is {Email}', + 'Send replay to {Email}', + 'My message is {Message}' + ] + }, + function(request,response){ + if (!Name) Name = response.slots['Name']; + if (!Email) Email = response.slots['Email']; + if (!Message) Message = response.slots['Message']; + let Data = reponse.slots['Data']; + + //TODO : Responses could be configurable for each skill ? + + if (State === 1){ + //Was waiting for name, so if Name is null, name is probably in Data + if ((!Name && Data) || Name){ + //got the name, let's continue for the email + State = 2; + return response.say('Ok ' + Name + '. What is your email ?').shouldEndSession(false); + }else{ + //Something is wrong, ask for name again + return response.say('Sorry, I didnt understand your name. Can you say it again ?').shouldEndSession(false); + } + }else if (State === 2){ + //was waiting for email, so if Email is null, email is probably in Data + if ((!Email && Data) || Email){ + //Got the email, first verify email and than continue to message + + //TODO : verify email + State = 3; + return response.say('Great. Whats the message ?').shouldEndSession(false); + }else{ + //Something is wrong, ask for the email again + return response.say('Sorry, I didnt understan you email. Can you say it again ?').shouldEndSession(false); + } + }else if (State === 3){ + //Was waiting for message, so if Message is null, message is probably in Data + if ((!Message && Data) || Message){ + //Ok, we got all informations. Exit email intent + State = 0; + //TODO : Send email + return response.say('Message sent. Someone will contact you ASAP'); + } + } + + } + ); + }) .catch (err => { console.log (err); @@ -92,14 +188,15 @@ router.get ('/deleteSkill/:skillID', async (req, res, next) => { router.post ('/updateSkill/:id', async (req, res, next) => { let id = req.params.id; - let skill = req.body; + let dataFromWeb = JSON.stringify(req.body); + let skill = JSON.parse(dataFromWeb); + let updateOnAmazon = skill.updateOnAmazon; + delete skill.updateOnAmazon; delete skill._id; console.log('id = ' + id); if (id !== '-1') { - amazonHelper - .updateSkill (skill) - .then (amazonResult => { - console.log('amazon result : ' + amazonResult); + if (updateOnAmazon){ + amazonHelper.updateSkill(skill).then(amazonResult=>{ if (amazonResult === 200 || amazonResult === 202) { //Skill uploaded, it's ok to update databaseI databaseHelper @@ -109,16 +206,24 @@ router.post ('/updateSkill/:id', async (req, res, next) => { updateIntentsJSON (); }) .catch (e => { - res.json ({result: -1, message: 'ok'}); + res.json ({result: -1, message: 'error'}); }); - } else { - res.json ({result: -1, message: 'Amazon result ' + amazonResult}); } - }) - .catch (e => { - //skill upload went wrong, don't update database, send error + }).catch(e=>{ res.json ({result: -1, message: e}); }); + }else{ + databaseHelper + .updateSkill (id, skill) + .then (result => { + res.json ({result: 0, message: 'ok'}); + updateIntentsJSON (); + }) + .catch (e => { + res.json ({result: -1, message: 'error'}); + }); + } + } else { //no new skills for now } diff --git a/backend/helpers/amazon.js b/backend/helpers/amazon.js index 26ee7e2..24900e0 100644 --- a/backend/helpers/amazon.js +++ b/backend/helpers/amazon.js @@ -81,6 +81,47 @@ var generateInteractionModel = function (skill) { allIntents.push ({name: intent.intentName, samples: intent.questions}); }); + //Special Email Intents : + allIntents.push({ + name: 'EmailIntentLaunch', + slots:[], + samples: [ + 'I want to send a message', + 'I would like to send a message', + 'I would like to leave a message', + 'Leave a message' + ] + }); + allIntents.push({ + name: 'EmailIntent', + slots:[ + { + name: 'Name', + type: 'AMAZON.Person' + }, + { + name: 'Email', + type: 'AMAZON.LITERAL' + }, + { + name: 'Message', + type: 'AMAZON.LITERAL' + }, + { + name: 'Data', + type: 'AMAZON.LITERAL' + } + ], + samples: [ + 'My name is {Name}', + 'I am {Name}', + '{Data}', + 'My email is {Email}', + 'Send replay to {Email}', + 'My message is {Message}' + ] + }); + result.interactionModel = {}; result.interactionModel.languageModel = { diff --git a/web/src/App.js b/web/src/App.js index 856efac..1a08693 100644 --- a/web/src/App.js +++ b/web/src/App.js @@ -4,9 +4,13 @@ import './css/popup.css'; import IntentList from './components/IntentList'; import IntentDetails from './components/IntentDetails'; import LaunchRequest from './components/LaunchRequest'; +import Contact from './components/Contact'; import Popup from 'react-popup'; - import {getSkill, updateSkill} from './lib/api' +import { + NEW_INTENT_SELECTED_INDEX, + LAUNCH_REQUEST_SELECTED_INDEX, + CONTACT_SELECTED_INDEX} from './config' class App extends Component { @@ -20,8 +24,8 @@ class App extends Component { invocationAnswer:'We are saburly', allIntents:[], selectedIntent: {intentName:'',questions:[''],answer:''}, - selectedIndex:-1, - launchRequest:false, + selectedIndex:NEW_INTENT_SELECTED_INDEX, + contactEmail:'', waiting: false }; @@ -30,7 +34,7 @@ class App extends Component { if (jResult===undefined) return; this.setState({ skillID:jResult.skillID,skillName:jResult.skillName, invocationName: jResult.invocationName, invocationAnswer: jResult.invocationAnswer, - allIntents: jResult.intents}) + allIntents: jResult.intents, contactEmail: jResult.contactEmail}) }) this.handleIntentClick = this.handleIntentClick.bind(this); @@ -41,62 +45,67 @@ class App extends Component { this.handleSaveLaunchRequestClick = this.handleSaveLaunchRequestClick.bind(this); this.createSkill = this.createSkill.bind(this); this.sendSkill = this.sendSkill.bind(this); + this.handleContactClick = this.handleContactClick.bind(this); + this.handleSaveEmailClick = this.handleSaveEmailClick.bind(this); } render() { - - if(this.state.launchRequest){ - return ( -
- -
-

Tell All

-
- - - - + return( +
+ +
+

Tell All

- ); - }else{ - return ( -
- -
-

Tell All

-
- - - - -
- ); - } + + + {( + ()=>{ + if (this.state.selectedIndex===LAUNCH_REQUEST_SELECTED_INDEX){ + return ( + + + ); + }else if (this.state.selectedIndex===CONTACT_SELECTED_INDEX){ + return ( + + + ); + }else{ + return( + + + ); + } + } + )()} +
+ ); } - createSkill(intents, name, answer){ + createSkill(intents, name, answer, email, updateOnAmazon){ return { _id: this.state._id, skillID: this.state.skillID, intents: intents, - invocationName: (name===undefined) ? this.state.invocationName : name, - invocationAnswer: (answer===undefined)? this.state.invocationAnswer: answer + invocationName: name, + invocationAnswer: answer, + contactEmail: email, + updateOnAmazon: updateOnAmazon }; } @@ -105,12 +114,21 @@ class App extends Component { } handleLaunchRequestClick(){ - this.setState({selectedIndex: -2, launchRequest:true}); + this.setState({selectedIndex: LAUNCH_REQUEST_SELECTED_INDEX}); + } + + handleContactClick(){ + this.setState({selectedIndex: CONTACT_SELECTED_INDEX}) } handleSaveLaunchRequestClick(name, answer){ this.setState({waiting:true, invocationName:name, invocationAnswer: answer}); - this.sendSkill(this.state.allIntents,true,{waiting:false},{waiting:false},name,answer); + this.sendSkill(this.state.allIntents,true,{waiting:false},{waiting:false},name,answer,this.state.contactEmail,true); + } + + handleSaveEmailClick(email){ + this.setState({waiting:true}); + this.sendSkill(this.state.allIntents,true,{contactEmail: email, waiting:false},{waiting:false},this.state.invocationName,this.state.invocationAnswer,email,false); } handleDeleteIntentClick(selectedIntent){ @@ -128,7 +146,7 @@ class App extends Component { this.setState({waiting:true}); let newState = {allIntents: newAllIntents, selectedIntent: {intentName:'', questions:[''],answer:''}, waiting:false}; - this.sendSkill(newAllIntents,true,newState,{waiting:false}); + this.sendSkill(newAllIntents,true,newState,{waiting:false},this.state.invocationName,this.state.invocationAnswer,this.state.contactEmail,true); }catch(e){ console.log("error : " + e); @@ -142,7 +160,7 @@ class App extends Component { let newAllIntents = JSON.parse(newAllIntentsJSON); let newState = null; - if (this.state.selectedIndex === -1){ + if (this.state.selectedIndex === NEW_INTENT_SELECTED_INDEX){ //new intent newAllIntents.push(selectedIntent); newState = {allIntents: newAllIntents, selectedIntent: selectedIntent, selectedIndex: newAllIntents.length-1, waiting:false}; @@ -151,16 +169,16 @@ class App extends Component { newState = {allIntents: newAllIntents, selectedIntent: selectedIntent, waiting: false}; } this.setState({waiting:true}); - this.sendSkill(newAllIntents, true, newState, {waiting:false}); + this.sendSkill(newAllIntents, true, newState, {waiting:false}, this.state.invocationName,this.state.invocationAnswer,this.state.contactEmail, true); } handleAddIntentClick(){ - this.setState({allIntents: this.state.allIntents, selectedIndex: -1,launchRequest:false,selectedIntent: {intentName:'',questions:[''], answer:''}}); + this.setState({allIntents: this.state.allIntents, selectedIndex: NEW_INTENT_SELECTED_INDEX,launchRequest:false,selectedIntent: {intentName:'',questions:[''], answer:''}}); } - sendSkill(newAllIntents, showPopUp, resolveState, rejectState, newName, newAnswer){ + sendSkill(newAllIntents, showPopUp, resolveState, rejectState, newName, newAnswer, email, updateOnAmazon){ return new Promise((resolve,reject)=>{ - updateSkill(this.createSkill(newAllIntents,newName,newAnswer)).then(l=>l.text()).then(result=>{ + updateSkill(this.createSkill(newAllIntents,newName,newAnswer,email,updateOnAmazon)).then(l=>l.text()).then(result=>{ let jResult = JSON.parse(result); if (jResult.result !== 0){ if (showPopUp) Popup.alert('Model was not saved. Please try again'); diff --git a/web/src/components/Contact.js b/web/src/components/Contact.js new file mode 100644 index 0000000..31e6b60 --- /dev/null +++ b/web/src/components/Contact.js @@ -0,0 +1,48 @@ +import React, { Component } from 'react'; +import {Button, TextField} from 'react-md'; +import '../css/Intent.css' +import {EMAIL_MAX_LENGTH} from '../config'; + +class Contact extends Component { + constructor(props){ + super(props); + + this.state = {contactEmail: props.contactEmail}; + + this.handleEmailEdit = this.handleEmailEdit.bind(this); + } + + componentWillReceiveProps(props){ + this.setState({contactEmail: props.contactEmail}); + } + + render() { + return ( +
+
Contact address will be used for direct messaging through Alexa
+ +

+

+

+ +
+ ); + } + + handleEmailEdit(e){ + if (e.length === EMAIL_MAX_LENGTH) return; + this.setState({contactEmail: e}); + } +} + +export default Contact; diff --git a/web/src/components/IntentList.js b/web/src/components/IntentList.js index 53943c3..e77032a 100644 --- a/web/src/components/IntentList.js +++ b/web/src/components/IntentList.js @@ -2,6 +2,9 @@ import React, { Component } from 'react'; import {Button} from 'react-md'; import IntentItem from './IntentItem'; import '../css/Intent.css' +import { + LAUNCH_REQUEST_SELECTED_INDEX, + CONTACT_SELECTED_INDEX} from '../config' class IntentList extends Component { constructor (props){ @@ -17,9 +20,15 @@ class IntentList extends Component { render() { return (
- + + + +

Intents

diff --git a/web/src/config.js b/web/src/config.js index 1a4de76..ac40a2b 100644 --- a/web/src/config.js +++ b/web/src/config.js @@ -9,3 +9,9 @@ export const INTENT_TITLE_TOOLTIP_DELAY = 700; export const INVOCATION_NAME_MAX_LENGTH = 15; export const INVOCATION_ANSWER_MAX_LENGTH = 100; + +export const EMAIL_MAX_LENGTH = 100; + +export const NEW_INTENT_SELECTED_INDEX = -1; +export const LAUNCH_REQUEST_SELECTED_INDEX = -2; +export const CONTACT_SELECTED_INDEX = -3; \ No newline at end of file diff --git a/web/src/css/Intent.css b/web/src/css/Intent.css index 2556df7..bd94c64 100644 --- a/web/src/css/Intent.css +++ b/web/src/css/Intent.css @@ -24,6 +24,20 @@ height: 50px; background-color: #f5f5f5; } +.Contact { + text-align: left; + color: #009b8a; + width: 100%; + height: 50px; + background-color: #d8d8d8; } + +.Contact-selected { + text-align: left; + color: #009b8a; + width: 100%; + height: 50px; + background-color: #f5f5f5; } + .AddIntent { float: right; margin: 12px; } diff --git a/web/src/css/Intent.scss b/web/src/css/Intent.scss index d755d26..3b35a99 100644 --- a/web/src/css/Intent.scss +++ b/web/src/css/Intent.scss @@ -31,6 +31,22 @@ $minHeight : calc(100vh - 80px); background-color: #f5f5f5; } +.Contact{ + text-align: left; + color: #009b8a; + width: 100%; + height: 50px; + background-color: #d8d8d8 +} + +.Contact-selected{ + text-align: left; + color: #009b8a; + width: 100%; + height: 50px; + background-color: #f5f5f5; +} + .AddIntent{ float: right; margin: 12px; From 4c8c1c5e0efb51c17ae2cd7e6395c91cff1cf998 Mon Sep 17 00:00:00 2001 From: Bilal Date: Thu, 11 Jan 2018 14:05:44 +0000 Subject: [PATCH 30/83] change utterances to correct form;change intents to mimic dialog; --- backend/config.js | 4 ++-- backend/express.js | 43 +++++++++++++++++++++++++-------------- backend/helpers/amazon.js | 34 +++++++++++++++++++------------ web/src/App.js | 4 +++- web/src/config.js | 4 ++-- 5 files changed, 56 insertions(+), 33 deletions(-) diff --git a/backend/config.js b/backend/config.js index 3edee7d..5c41336 100644 --- a/backend/config.js +++ b/backend/config.js @@ -8,8 +8,8 @@ config.REFRESH_TOKEN = 'Atzr|IwEBICA3kDhfSJxlwvnQp9AD1o115AC_KBbFd5GBg8oN_QHWn2o config.TOKEN_EXPIRES_IN = 1515100500; config.SKILL_ID = 'amzn1.ask.skill.efbf0564-a732-4ba9-958f-57939138adae'; -//config.SKILL_DB_ID = '5a5016e775becaef2015da10'; //for server -config.SKILL_DB_ID = '5a232fb86ce046c749739455'; //for local +config.SKILL_DB_ID = '5a5016e775becaef2015da10'; //for server +//config.SKILL_DB_ID = '5a232fb86ce046c749739455'; //for local config.CLIENT_ID = 'amzn1.application-oa2-client.c748ca56ded04a95b236979898585ff7'; config.CLIENT_SECRET = '6dea8125cecd049d3c4cff7bb5bdfd3ff17bc6fed246c4c8f6b519d9ed08d0b3'; diff --git a/backend/express.js b/backend/express.js index 3a32745..91154fc 100644 --- a/backend/express.js +++ b/backend/express.js @@ -92,25 +92,27 @@ var updateIntentsJSON = function () { //This is problem only if we introduce slot options for regular intents for users alexaApp.intent('EmailIntent',{ slots:{ - 'Name':'AMAZON.Person', + 'Name':'AMAZON.US_FIRST_NAME', 'Email' : 'AMAZON.LITERAL', 'Message': 'AMAZON.LITERAL', 'Data': 'AMAZON.LITERAL' }, utterances: [ - 'My name is {Name}', - 'I am {Name}', - '{Data}', - 'My email is {Email}', - 'Send replay to {Email}', - 'My message is {Message}' + 'My name is {-|Name}', + 'I am {-|Name}', + '{dawdw at dwd wdw|Data}', + 'My email is {wadwwdw at wadwwd wdw|Email}', + 'Send replay to {fkofkeofe at dppfam wd|Email}', + 'My message is {Quick brown fox jumps over lazy dog|Message}' ] }, function(request,response){ - if (!Name) Name = response.slots['Name']; - if (!Email) Email = response.slots['Email']; - if (!Message) Message = response.slots['Message']; - let Data = reponse.slots['Data']; + let Data = undefined; + try{if (!Name) Name = request.slot('Name');}catch(e){console.log('Error. No name slot ');Name=undefined;} + try{if (!Email) Email = request.slot('Email');}catch(e){console.log('Error. No Email slot');Email=undefined;} + try{if (!Message) Message = request.slot('Message');}catch(e){console.log('Error. No Message slot');Message=undefined;} + try{Data = request.slot('Data');}catch(e){console.log('Error. No Data slot');Data=undefined;} + console.log('State : ' + State); //TODO : Responses could be configurable for each skill ? @@ -118,6 +120,7 @@ var updateIntentsJSON = function () { //Was waiting for name, so if Name is null, name is probably in Data if ((!Name && Data) || Name){ //got the name, let's continue for the email + if (!Name) Name = Data; State = 2; return response.say('Ok ' + Name + '. What is your email ?').shouldEndSession(false); }else{ @@ -128,7 +131,7 @@ var updateIntentsJSON = function () { //was waiting for email, so if Email is null, email is probably in Data if ((!Email && Data) || Email){ //Got the email, first verify email and than continue to message - + if (!Email) Email = Data; //TODO : verify email State = 3; return response.say('Great. Whats the message ?').shouldEndSession(false); @@ -140,11 +143,18 @@ var updateIntentsJSON = function () { //Was waiting for message, so if Message is null, message is probably in Data if ((!Message && Data) || Message){ //Ok, we got all informations. Exit email intent + if (!Message) Message=Data; State = 0; //TODO : Send email + console.log('Name : ' + Name + ' | Email : ' + Email + ' | Message : ' + Message); return response.say('Message sent. Someone will contact you ASAP'); - } - } + }else{ + //Something is wrong, ask for the message again + return response.say('Sorry, I didnt understand your message. Can you say it again ?').shouldEndSession(false); + } + }else{ + console.log('State strange ! ' + State); + } } ); @@ -197,6 +207,7 @@ router.post ('/updateSkill/:id', async (req, res, next) => { if (id !== '-1') { if (updateOnAmazon){ amazonHelper.updateSkill(skill).then(amazonResult=>{ + console.log('Amazon : ' + amazonResult); if (amazonResult === 200 || amazonResult === 202) { //Skill uploaded, it's ok to update databaseI databaseHelper @@ -208,7 +219,9 @@ router.post ('/updateSkill/:id', async (req, res, next) => { .catch (e => { res.json ({result: -1, message: 'error'}); }); - } + }else{ + res.json({result: -1, message: 'amazon error : ' + amazonResult}); + } }).catch(e=>{ res.json ({result: -1, message: e}); }); diff --git a/backend/helpers/amazon.js b/backend/helpers/amazon.js index 24900e0..d182c1c 100644 --- a/backend/helpers/amazon.js +++ b/backend/helpers/amazon.js @@ -49,6 +49,9 @@ var refreshTokens = function () { parsedResponse.access_token, parsedResponse.expires_in ); + console.log('Token refresh failed'); + console.log(body); + reject(body); }); }); }; @@ -81,6 +84,7 @@ var generateInteractionModel = function (skill) { allIntents.push ({name: intent.intentName, samples: intent.questions}); }); + //Special Email Intents : allIntents.push({ name: 'EmailIntentLaunch', @@ -92,36 +96,40 @@ var generateInteractionModel = function (skill) { 'Leave a message' ] }); + allIntents.push({ name: 'EmailIntent', slots:[ { - name: 'Name', - type: 'AMAZON.Person' + 'name': 'Name', + 'type': 'AMAZON.US_FIRST_NAME' }, { - name: 'Email', - type: 'AMAZON.LITERAL' + 'name': 'Email', + 'type': 'AMAZON.LITERAL' }, { - name: 'Message', - type: 'AMAZON.LITERAL' + 'name': 'Message', + 'type': 'AMAZON.LITERAL' }, { - name: 'Data', - type: 'AMAZON.LITERAL' + 'name': 'Data', + 'type': 'AMAZON.LITERAL' } ], + + samples: [ 'My name is {Name}', 'I am {Name}', - '{Data}', - 'My email is {Email}', - 'Send replay to {Email}', - 'My message is {Message}' + '{exampleww at wwdwdw|Data}', + 'My email is {example at efefegedd|Email}', + 'Send replay to {example at abcdefg|Email}', + 'My message is {The quick brown fox jumps over the lazy dog.The quick brown fox jumps over the lazy dog.The quick brown fox jumps over the lazy dog.|Message}' ] + }); - + result.interactionModel = {}; result.interactionModel.languageModel = { diff --git a/web/src/App.js b/web/src/App.js index 1a08693..63aa044 100644 --- a/web/src/App.js +++ b/web/src/App.js @@ -17,7 +17,7 @@ class App extends Component { constructor(props){ super(props); - this.state={_id:'5a232fb86ce046c749739455', + this.state={_id:'5a5016e775becaef2015da10', skillID:'', skillName:'', invocationName:'Saburly', @@ -181,6 +181,7 @@ class App extends Component { updateSkill(this.createSkill(newAllIntents,newName,newAnswer,email,updateOnAmazon)).then(l=>l.text()).then(result=>{ let jResult = JSON.parse(result); if (jResult.result !== 0){ + console.log(jResult.result); if (showPopUp) Popup.alert('Model was not saved. Please try again'); this.setState(rejectState); //reject('Error code : ' + jResult.result); @@ -190,6 +191,7 @@ class App extends Component { resolve(); } }).catch(e=>{ + console.log('error : ' + e); if (showPopUp) Popup.alert('Model was not saved. Please try again'); this.setState(rejectState); //reject(e); diff --git a/web/src/config.js b/web/src/config.js index ac40a2b..b7d4e2e 100644 --- a/web/src/config.js +++ b/web/src/config.js @@ -1,4 +1,4 @@ -export const BASE_URL = 'localhost:5000'; +export const BASE_URL = 'tellall.saburly.com'; export const INTENT_NAME_MAX_LENGTH = 30; export const QUESTION_MAX_LENGTH = 150; @@ -14,4 +14,4 @@ export const EMAIL_MAX_LENGTH = 100; export const NEW_INTENT_SELECTED_INDEX = -1; export const LAUNCH_REQUEST_SELECTED_INDEX = -2; -export const CONTACT_SELECTED_INDEX = -3; \ No newline at end of file +export const CONTACT_SELECTED_INDEX = -3; From b80843cb97bb2effb7508eab68522800be38173f Mon Sep 17 00:00:00 2001 From: GotPPay Date: Fri, 12 Jan 2018 01:56:17 +0100 Subject: [PATCH 31/83] ... --- README.md | 102 +++++++- backend/SaburlySkillInteractionModel.json | 60 ----- backend/components/alexa.js | 205 ++++++++++++++++ backend/{ => config}/config.js | 0 backend/config/constants.js | 25 ++ backend/express.js | 268 --------------------- backend/helpers/amazon.js | 8 +- backend/helpers/database.js | 25 +- backend/server.js | 122 ++++++++++ web/README.md | 82 ------- web/src/App.js | 9 +- web/src/components/Contact.js | 2 +- web/src/components/IntentDetails.js | 2 +- web/src/components/IntentItem.js | 2 +- web/src/components/IntentList.js | 2 +- web/src/components/LaunchRequest.js | 2 +- web/src/config/config.js | 3 + web/src/{config.js => config/constants.js} | 7 +- web/src/lib/api.js | 2 +- web/src/logo.svg | 7 - 20 files changed, 482 insertions(+), 453 deletions(-) delete mode 100644 backend/SaburlySkillInteractionModel.json create mode 100644 backend/components/alexa.js rename backend/{ => config}/config.js (100%) create mode 100644 backend/config/constants.js delete mode 100644 backend/express.js create mode 100644 backend/server.js delete mode 100644 web/README.md create mode 100644 web/src/config/config.js rename web/src/{config.js => config/constants.js} (89%) delete mode 100644 web/src/logo.svg diff --git a/README.md b/README.md index ec7ab2c..caec0bf 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,98 @@ -first terminal -cd web -npm install -npm start +To obtain new Auth Code : -second terminal -cd backend -npm install -node express.js +https://www.amazon.com/ap/oa?client_id=amzn1.application-oa2-client.c748ca56ded04a95b236979898585ff7&scope=alexa::ask:skills:readwrite alexa::ask:models:readwrite alexa::ask:skills:test&response_type=code&redirect_uri=https://layla.amazon.com/api/skill/link/M2ODJY6EXOY6KO +Response URL (Decoded) : + +https://layla.amazon.com/api/skill/link/M2ODJY6EXOY6KO?code=ANCgZUfEFdlRRkpSNFuA&scope=alexa::ask:skills:readwrite alexa::ask:models:readwrite alexa::ask:skills:test + +Code : ANCgZUfEFdlRRkpSNFuA +======================================= + +Now to get Access Token : + +Send a POST request to https://api.amazon.com/auth/o2/token with the following parameters + + - HTTP Header Parameters + + Content-Type: application/x-www-form-urlencoded + + - HTTP Body Parameters + + grant_type: authorization_code + code: The authorization code that was returned in the response. + client_id: The product’s client ID. To access this information, navigate to Amazon’s Developer Console. After you’ve logged in, click Get Started > under Alexa Voice Service, then click Edit next to a registered product (or create a new one). From the left navigation select Security Profile. This page contains both Client ID and Client Secret for your product. + client_secret: The product’s client secret. To access this information, navigate to Amazon’s Developer Console. After you’ve logged in, click Get Started > under Alexa Voice Service, then click Edit next to a registered product (or create a new one). From the left navigation select Security Profile. This page contains both Client ID and Client Secret for your product. + redirect_uri: One of the return URIs that you added to your app’s security profile when signing up. + +Response : + +{ + "access_token": "Atza|IwEBIBe6gDqrrowEEav6N-_6s4NztYeP3oG8PGWmu8ZiZw6lbOh3wNla3TK6pY-VEpT1d8an-dVf_n3kXJzVFsNo_4xBfZyFHGoCTDTFjs3yBRul4PVdBOhwwiH3-sgRLcUofZbe2oE06GmTcbfYtaStfXpQI5dfpldfnsJg_CvhSA6AHb_snJT3F6lyXzbV076d_3cYUMJxFldJGnYcviNHHxjjmuQTD06hhGzCbAxxe9eBmkuopRsNfyedLT2UlKP_ublah9CUGA3AdIX_3Iuke82jMwGnNl9gv7pbaDNEjAbj7IQSl3B08uuREtJq-oTBOjALNXRvFxTJmQjZwXNf9eHC7fSHJDdEPdZQU0AcffRQObAyAkUuL6Jv39OHzhb3Q64-zzoyODqnJyLP5SQZ2JVF53Kc_cTBqjIc9pXljqe7yEVk6JDs7q1zKbBibx_AQm57TO79IzWyLBzBMlYL5HdTsqEfRzLeDw2tws-hGMgkx2HWfdbYnmf5Qb4SyIhzvmmdfPLg3MVKTxjIBu1rx0xf3n0PLZP1EO6jsJPoMRPg77Gm4oit5Zp6s37ek3A3Vxh-ntoASpkrkxGTG9kVtRNt", + "refresh_token": "Atzr|IwEBICA3kDhfSJxlwvnQp9AD1o115AC_KBbFd5GBg8oN_QHWn2or5xFQ09BruuK6a07tGHtTt_9q2Y21mnOMH4RDtYXTVG9ADgLE6mHWKZFxYVwt3kHMiUJdY5lJcsOtWLoblrS-bJ0BEXXK5nVDt_bSI5IB7NUf-9QVZxhovRH_ANSxdTjJT0_rMIAZY3WEj68FEap49q_pg72BhnxHVZD2TC3zvX96_DN65HE5SoSgT7OiMAeiJewB_SyemW_HxBwaB-_X-G1ifOtnrzZ4gXTpOrEUlHI2YPuvRMBMtmz1h-nXDZYv3vwU3RA57Qj_ZNVcScj8_RXf2xq8w48v0PzZFXYBSalfnqPq6eUvSSbAJUp6bB8y596JlvR5dFQe_Z--X0Gkfo85IcyrI9D44vK9sJhrGhCVi2FDDa8pHczmNSen99JYZvDif-zpYzgbcymAkOV0gC7JvYMxlZfETT3NTBy7eVA7fJI1SZaeA_qW49xRcBkZBu5gkqTpeGWUU1cGr2aXRVVmXGM22NfV5E7KzvEBsCeHml_tCfxZeKY8Msd8hJb0Cd59u-_hsuc8oNjsOpIdFF976dY3uTmAgHWpG2PH", + "token_type": "bearer", + "expires_in": 3600 +} + +================================ + +Now to get new Access token using refresh token : + +Send a POST request to https://api.amazon.com/auth/o2/token with the following parameters: + + - HTTP Header Parameters + + Content-Type: application/x-www-form-urlencoded + + - HTTP Body Parameters + + grant_type: refresh_token + refresh_token: The URL-encoded refresh token returned with the last request for a new access token. + client_id: To access this information, navigate to Amazon’s Developer Console. After you’ve logged in, click Get Started > under Alexa Voice Service, then click Edit next to a registered product (or create a new one). From the left navigation select Security Profile. This page contains both Client ID and Client Secret for your product. + client_secret: The website’s client secret. To access this information, navigate to Amazon’s Developer Console. After you’ve logged in, click Get Started > under Alexa Voice Service, then click Edit next to a registered product (or create a new one). From the left navigation select Security Profile. This page contains both Client ID and Client Secret for your product. + +Response : + +{ + "access_token": "Atza|IwEBILtBe3hrHovrMx7Oivng-RB2EKzvCm_epXJE2HXPMQzXTFqK10Zrqt-Z8paeRoLQBqbLCmqWvcr5RTNgw9qjtfzOTsOrXC1VKqKmxpqHTrJyn2TLGsCzFjBDfADNjCyufWTf2ZlsSzjxW2GiqCHlwoPSd9pFrLavtRThrm1J-5KvnFrj-yD-tYTSwrgX5W5p2SrjQxoE3aP5b96z6p8GvCL9lM1pddafAxkHb22A3IzR-pYGmEijb4ksRuaIf4WCNwssWV6GBIB2oJA5CU-Dtd2mOZZ5-dYpSSeCHyGumTYecTxxMVSdiVjCqB8WT6AtvvutWFQQoldHjJmIwBsTZP-iQcl-UyajOZJ03GqRUym5Hp-49uByzVG-MfR_Z5qVmYjjsLQEOLCY9kPVnmRGnOTj6YPjrHXibd6P8TQOMh4VTcgFpg-afKKABP6EeDwok1t2ivuYh5OJju-B1A6gzhMi4vQJYKq107e0QMYBBhrf_OqCgMbfnQZ8j40qocVGID5YWv8uk5wKyI61LrbzrTltmzxzNemzqbSBzwAlfNS6GW-jVjg8svsi1lb_EVRbhyOoWJWX3mEd-5GDYyUcyInleiAR0aIHVP94pZxqdiCamA", + "refresh_token": "Atzr|IwEBICA3kDhfSJxlwvnQp9AD1o115AC_KBbFd5GBg8oN_QHWn2or5xFQ09BruuK6a07tGHtTt_9q2Y21mnOMH4RDtYXTVG9ADgLE6mHWKZFxYVwt3kHMiUJdY5lJcsOtWLoblrS-bJ0BEXXK5nVDt_bSI5IB7NUf-9QVZxhovRH_ANSxdTjJT0_rMIAZY3WEj68FEap49q_pg72BhnxHVZD2TC3zvX96_DN65HE5SoSgT7OiMAeiJewB_SyemW_HxBwaB-_X-G1ifOtnrzZ4gXTpOrEUlHI2YPuvRMBMtmz1h-nXDZYv3vwU3RA57Qj_ZNVcScj8_RXf2xq8w48v0PzZFXYBSalfnqPq6eUvSSbAJUp6bB8y596JlvR5dFQe_Z--X0Gkfo85IcyrI9D44vK9sJhrGhCVi2FDDa8pHczmNSen99JYZvDif-zpYzgbcymAkOV0gC7JvYMxlZfETT3NTBy7eVA7fJI1SZaeA_qW49xRcBkZBu5gkqTpeGWUU1cGr2aXRVVmXGM22NfV5E7KzvEBsCeHml_tCfxZeKY8Msd8hJb0Cd59u-_hsuc8oNjsOpIdFF976dY3uTmAgHWpG2PH", + "token_type": "bearer", + "expires_in": 3600 +} + +======================= +======================= + +Prerequests for step 3 (run on server): requires running mongodb service + Database (tellall) with collection (skill_list) + * Insert dummy skill with : db.skill_list.insert({"skillID" : "amzn1.ask.skill.efbf0564-a732-4ba9-958f-57939138adae", "intents" : [ { "intentName" : "GetFirstQuestion", "questions" : [ "tell me something about projects", "tell me all about projects" ], "answer" : "blablabla bla bla" }, { "intentName" : "GetThirdQuestion", "questions" : [ "Give me third question" ], "answer" : "This is answer to the third question" } ], "invocationName" : "Saburly", "invocationAnswer" : "We are Saburly team one" }) + + *obtain _id and change in web/src/App.js, and also skill_db_id in backend/config.js + *enter web/ dir and run "npm run build" + + Database (tellall) with collection (token_list) + * Insert tokens with : db.token_list.insert({"id" : 1, "refresh_token" : "...", "access_token" : "...", "expires_in" : 1515173601.754 }) + (Change refresh_token and access_token dots with real ones) + + Set skill_id, client_id and client_secret to appropriate values in backend/config.js + Set base_url to "tellall.saburly.com" in web/src/config. + + Start backend service from backend/ running "node express.js" + + +====== +for local testing : + +first terminal : + *cd web + *npm start + +second terminal : + * cd backend + *npm install + *node express.js + + + diff --git a/backend/SaburlySkillInteractionModel.json b/backend/SaburlySkillInteractionModel.json deleted file mode 100644 index ff9fdd7..0000000 --- a/backend/SaburlySkillInteractionModel.json +++ /dev/null @@ -1,60 +0,0 @@ -{ - "languageModel": { - "intents": [ - { - "name": "AMAZON.CancelIntent", - "samples": [] - }, - { - "name": "AMAZON.HelpIntent", - "samples": [] - }, - { - "name": "AMAZON.StopIntent", - "samples": [] - }, - { - "name": "GetProcessIntent", - "samples": [ - "tell me your process", - "what is your process", - "how do you work", - "about your proces" - ], - "slots": [] - }, - { - "name": "GetProjectsIntent", - "samples": [ - "list me your projects", - "show me what you have done", - "what did you do so far", - "what are your projects", - "your project" - ], - "slots": [] - }, - { - "name": "GetServicesIntent", - "samples": [ - "what are your services", - "what services do you ofer", - "what you do", - "your service", - "tell me service" - ], - "slots": [] - }, - { - "name": "GetTechnologiesIntent", - "samples": [ - "list me your technologies", - "give me your technologies", - "what you know" - ], - "slots": [] - } - ], - "invocationName": "saburly" - } -} \ No newline at end of file diff --git a/backend/components/alexa.js b/backend/components/alexa.js new file mode 100644 index 0000000..93bbcc2 --- /dev/null +++ b/backend/components/alexa.js @@ -0,0 +1,205 @@ +var alexa = require ('alexa-app'); +const config = require ('../config/config'); +var databaseHelper = require ('../helpers/database'); + +//User data for sending message, this is skill-related and will be in some skill container +var Name = null; +var Email = null; +var Message = null; +var State = 0; // states should be defined in seperate file. (Not sending message, Waiting for name, Waiting for email, Waiting for message) +//For now (this is not long term solution) +// 0 : Not sending Message +// 1 : Waiting for name +// 2 : Waiting for email +// 3 : Waiting for message + +var alexaApp = new alexa.app ('saburly'); // this means we still work with one skill + +module.exports = { + init: function (express) { + + alexaApp.express ({ + expressApp: express, + + // verifies requests come from amazon alexa. Must be enabled for production. + // You can disable this if you're running a dev environment and want to POST + // things to test behavior. enabled by default. + checkCert: false, + + // sets up a GET route when set to true. This is handy for testing in + // development, but not recommended for production. disabled by default + debug: true, + }); + }, + + updateIntentsJSON: function () { + databaseHelper + .loadSkill (config.SKILL_DB_ID) + .then (skill => { + skill.intents.map (intent => { + alexaApp.intent ( + intent.intentName, + { + slots: [], + utterances: intent.questions, + }, + function (request, response) { + return response.say (intent.answer).shouldEndSession (false); + } + ); + }); + + alexaApp.launch ((request, response) => { + return response.say (skill.invocationAnswer).shouldEndSession (false); + }); + + alexaApp.intent ( + 'EmailIntentLaunch', + { + slots: [], + utterances: [ + 'I want to send a message', + 'I would like to send a message', + 'I would like to leave a message', + 'Leave a message', + ], + }, + function (request, response) { + Name = null; + Email = null; + Message = null; + State = 1; + + return response + .say ('Ok. What is your name') + .shouldEndSession (false); + } + ); + + //TODO : Watch out for this intent. It will make trouble with other regular intents + //if other intents have utterance with just one slot like {Data} + //It should be taken care somwhere before this, to check if Email Intent is invoked + //This is problem only if we introduce slot options for regular intents for users + alexaApp.intent ( + 'EmailIntent', + { + slots: { + Name: 'AMAZON.US_FIRST_NAME', + Email: 'AMAZON.LITERAL', + Message: 'AMAZON.LITERAL', + Data: 'AMAZON.LITERAL', + }, + utterances: [ + 'My name is {-|Name}', + 'I am {-|Name}', + '{dawdw at dwd wdw|Data}', + 'My email is {wadwwdw at wadwwd wdw|Email}', + 'Send replay to {fkofkeofe at dppfam wd|Email}', + 'My message is {Quick brown fox jumps over lazy dog|Message}', + ], + }, + function (request, response) { + let Data = undefined; + try { + if (!Name) Name = request.slot ('Name'); + } catch (e) { + console.log ('Error. No name slot '); + Name = undefined; + } + try { + if (!Email) Email = request.slot ('Email'); + } catch (e) { + console.log ('Error. No Email slot'); + Email = undefined; + } + try { + if (!Message) Message = request.slot ('Message'); + } catch (e) { + console.log ('Error. No Message slot'); + Message = undefined; + } + try { + Data = request.slot ('Data'); + } catch (e) { + console.log ('Error. No Data slot'); + Data = undefined; + } + console.log ('State : ' + State); + + //TODO : Responses could be configurable for each skill ? + + if (State === 1) { + //Was waiting for name, so if Name is null, name is probably in Data + if ((!Name && Data) || Name) { + //got the name, let's continue for the email + if (!Name) Name = Data; + State = 2; + return response + .say ('Ok ' + Name + '. What is your email ?') + .shouldEndSession (false); + } else { + //Something is wrong, ask for name again + return response + .say ( + 'Sorry, I didnt understand your name. Can you say it again ?' + ) + .shouldEndSession (false); + } + } else if (State === 2) { + //was waiting for email, so if Email is null, email is probably in Data + if ((!Email && Data) || Email) { + //Got the email, first verify email and than continue to message + if (!Email) Email = Data; + //TODO : verify email + State = 3; + return response + .say ('Great. Whats the message ?') + .shouldEndSession (false); + } else { + //Something is wrong, ask for the email again + return response + .say ( + 'Sorry, I didnt understan you email. Can you say it again ?' + ) + .shouldEndSession (false); + } + } else if (State === 3) { + //Was waiting for message, so if Message is null, message is probably in Data + if ((!Message && Data) || Message) { + //Ok, we got all informations. Exit email intent + if (!Message) Message = Data; + State = 0; + //TODO : Send email + console.log ( + 'Name : ' + + Name + + ' | Email : ' + + Email + + ' | Message : ' + + Message + ); + return response.say ( + 'Message sent. Someone will contact you ASAP' + ); + } else { + //Something is wrong, ask for the message again + return response + .say ( + 'Sorry, I didnt understand your message. Can you say it again ?' + ) + .shouldEndSession (false); + } + } else { + console.log ('State strange ! ' + State); + } + } + ); + }) + .catch (err => { + console.log (err); + alexaApp.launch ((request, response) => { + return response.say ('Sorry, there was no skill with that name'); + }); + }); + }, +}; diff --git a/backend/config.js b/backend/config/config.js similarity index 100% rename from backend/config.js rename to backend/config/config.js diff --git a/backend/config/constants.js b/backend/config/constants.js new file mode 100644 index 0000000..5895516 --- /dev/null +++ b/backend/config/constants.js @@ -0,0 +1,25 @@ +const constants = {}; + +constants.amazonResultCodes = { + ok:200, + accepted:202, + badRequest:400, + unauthorized:401, + notFound:404, + conflict:409, + payloadTooLarge:413 +} + +constants.apiResultCodes = { + genericError : -1, + ok:0, + amazonError:1, + databaseError:2, + +} + +constants.skillIDLength = 24; + + + +module.exports = constants; \ No newline at end of file diff --git a/backend/express.js b/backend/express.js deleted file mode 100644 index 91154fc..0000000 --- a/backend/express.js +++ /dev/null @@ -1,268 +0,0 @@ -var amazonHelper = require ('./helpers/amazon'); -var databaseHelper = require ('./helpers/database'); -const config = require ('./config'); -require ('isomorphic-fetch'); -var express = require ('express'); -var alexa = require ('alexa-app'); -var bodyParser = require ('body-parser'); -var MongoClient = require ('mongodb').MongoClient; -var ObjectID = require ('mongodb').ObjectID; - -const router = express.Router (); - -var app = express (); - -//User data for sending message, this is skill-related and will be in some skill container -var Name = null; -var Email = null; -var Message = null; -var State = 0; // states should be defined in seperate file. (Not sending message, Waiting for name, Waiting for email, Waiting for message) -//For now : -// 0 : Not sending Message -// 1 : Waiting for name -// 2 : Waiting for email -// 3 : Waiting for message - -// ALWAYS setup the alexa app and attach it to express before anything else. -var alexaApp = new alexa.app ('saburly'); // this means we still work with one skill - -alexaApp.express ({ - expressApp: app, - - // verifies requests come from amazon alexa. Must be enabled for production. - // You can disable this if you're running a dev environment and want to POST - // things to test behavior. enabled by default. - checkCert: false, - - // sets up a GET route when set to true. This is handy for testing in - // development, but not recommended for production. disabled by default - debug: true, -}); - -// now POST calls to /test in express will be handled by the app.request() function - -// from here on you can setup any other express routes or middlewares as nor - -app.set ('view engine', 'ejs'); - -var updateIntentsJSON = function () { - databaseHelper - .loadSkill (config.SKILL_DB_ID) - .then (skill => { - skill.intents.map (intent => { - - alexaApp.intent ( - intent.intentName, - { - slots: [], - utterances: intent.questions, - }, - function (request, response) { - return response.say (intent.answer).shouldEndSession(false); - } - ); - }); - - alexaApp.launch ((request, response) => { - return response.say (skill.invocationAnswer).shouldEndSession(false); - }); - - alexaApp.intent('EmailIntentLaunch',{ - slots:[], - utterances: [ - 'I want to send a message', - 'I would like to send a message', - 'I would like to leave a message', - 'Leave a message' - ] - }, - function(request,response){ - Name = null; - Email = null; - Message = null; - State = 1; - - return response.say('Ok. What is your name').shouldEndSession(false); - } - ); - - //TODO : Watch out for this intent. It will make trouble with other regular intents - //if other intents have utterance with just one slot like {Data} - //It should be taken care somwhere before this, to check if Email Intent is invoked - //This is problem only if we introduce slot options for regular intents for users - alexaApp.intent('EmailIntent',{ - slots:{ - 'Name':'AMAZON.US_FIRST_NAME', - 'Email' : 'AMAZON.LITERAL', - 'Message': 'AMAZON.LITERAL', - 'Data': 'AMAZON.LITERAL' - }, - utterances: [ - 'My name is {-|Name}', - 'I am {-|Name}', - '{dawdw at dwd wdw|Data}', - 'My email is {wadwwdw at wadwwd wdw|Email}', - 'Send replay to {fkofkeofe at dppfam wd|Email}', - 'My message is {Quick brown fox jumps over lazy dog|Message}' - ] - }, - function(request,response){ - let Data = undefined; - try{if (!Name) Name = request.slot('Name');}catch(e){console.log('Error. No name slot ');Name=undefined;} - try{if (!Email) Email = request.slot('Email');}catch(e){console.log('Error. No Email slot');Email=undefined;} - try{if (!Message) Message = request.slot('Message');}catch(e){console.log('Error. No Message slot');Message=undefined;} - try{Data = request.slot('Data');}catch(e){console.log('Error. No Data slot');Data=undefined;} - console.log('State : ' + State); - - //TODO : Responses could be configurable for each skill ? - - if (State === 1){ - //Was waiting for name, so if Name is null, name is probably in Data - if ((!Name && Data) || Name){ - //got the name, let's continue for the email - if (!Name) Name = Data; - State = 2; - return response.say('Ok ' + Name + '. What is your email ?').shouldEndSession(false); - }else{ - //Something is wrong, ask for name again - return response.say('Sorry, I didnt understand your name. Can you say it again ?').shouldEndSession(false); - } - }else if (State === 2){ - //was waiting for email, so if Email is null, email is probably in Data - if ((!Email && Data) || Email){ - //Got the email, first verify email and than continue to message - if (!Email) Email = Data; - //TODO : verify email - State = 3; - return response.say('Great. Whats the message ?').shouldEndSession(false); - }else{ - //Something is wrong, ask for the email again - return response.say('Sorry, I didnt understan you email. Can you say it again ?').shouldEndSession(false); - } - }else if (State === 3){ - //Was waiting for message, so if Message is null, message is probably in Data - if ((!Message && Data) || Message){ - //Ok, we got all informations. Exit email intent - if (!Message) Message=Data; - State = 0; - //TODO : Send email - console.log('Name : ' + Name + ' | Email : ' + Email + ' | Message : ' + Message); - return response.say('Message sent. Someone will contact you ASAP'); - }else{ - //Something is wrong, ask for the message again - return response.say('Sorry, I didnt understand your message. Can you say it again ?').shouldEndSession(false); - } - }else{ - console.log('State strange ! ' + State); - } - - } - ); - - }) - .catch (err => { - console.log (err); - alexaApp.launch ((request, response) => { - return response.say ('Sorry, there was no skill with that name'); - }); - }); -}; - -router.get ('/getSkill/:id', async (req, res, next) => { - const id = req.params.id; - - if (id.length !== 24) { - res.json ([]); - } else { - databaseHelper - .getSkill (id) - .then (result => { - res.json (result); - }) - .catch (err => { - res.json ([]); - }); - } -}); - -router.get ('/deleteSkill/:skillID', async (req, res, next) => { - databaseHelper - .deleteSkill (req.params.id) - .then (result => { - res.json (result); - }) - .catch (err => { - res.json (err); - }); -}); - -router.post ('/updateSkill/:id', async (req, res, next) => { - let id = req.params.id; - let dataFromWeb = JSON.stringify(req.body); - let skill = JSON.parse(dataFromWeb); - let updateOnAmazon = skill.updateOnAmazon; - delete skill.updateOnAmazon; - delete skill._id; - console.log('id = ' + id); - if (id !== '-1') { - if (updateOnAmazon){ - amazonHelper.updateSkill(skill).then(amazonResult=>{ - console.log('Amazon : ' + amazonResult); - if (amazonResult === 200 || amazonResult === 202) { - //Skill uploaded, it's ok to update databaseI - databaseHelper - .updateSkill (id, skill) - .then (result => { - res.json ({result: 0, message: 'ok'}); - updateIntentsJSON (); - }) - .catch (e => { - res.json ({result: -1, message: 'error'}); - }); - }else{ - res.json({result: -1, message: 'amazon error : ' + amazonResult}); - } - }).catch(e=>{ - res.json ({result: -1, message: e}); - }); - }else{ - databaseHelper - .updateSkill (id, skill) - .then (result => { - res.json ({result: 0, message: 'ok'}); - updateIntentsJSON (); - }) - .catch (e => { - res.json ({result: -1, message: 'error'}); - }); - } - - } else { - //no new skills for now - } -}); - -app.use (function (req, res, next) { - res.header ('Access-Control-Allow-Origin', '*'); - res.header ('Access-Control-Allow-Headers', 'Origin, Content-Type'); - res.header ('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); - res.header ('Access-Control-Allow-Credentials', 'true'); - next (); -}); - -app.use (bodyParser.json ()); -app.use ('/', router); - -MongoClient.connect (config.dbURL) - .then (database => { - databaseHelper.initModule (database); - - app.listen (config.PORT, () => { - console.log ('Express server running on port ' + config.PORT); - updateIntentsJSON (); - databaseHelper.loadTokens (); - }); - }) - .catch (e => { - console.log ('error : ' + e); - }); diff --git a/backend/helpers/amazon.js b/backend/helpers/amazon.js index d182c1c..ab31000 100644 --- a/backend/helpers/amazon.js +++ b/backend/helpers/amazon.js @@ -1,11 +1,10 @@ require ('isomorphic-fetch'); -const config = require ('../config'); +const config = require ('../config/config'); var request = require ('request'); var databaseHelper = require ('./database'); var getBuildStatus = function (skillID) { - try { - fetch ( + fetch ( `https://api.amazonalexa.com/v0/skills/${skillID}/interactionModel/locales/en-US/status`, { method: 'GET', @@ -18,9 +17,6 @@ var getBuildStatus = function (skillID) { .then (result => { return result; }); - } catch (e) { - console.log ('err : ' + e); - } }; var refreshTokens = function () { diff --git a/backend/helpers/database.js b/backend/helpers/database.js index 0b504a8..bd662a2 100644 --- a/backend/helpers/database.js +++ b/backend/helpers/database.js @@ -1,4 +1,4 @@ -const config = require ('../config'); +const config = require ('../config/config'); var ObjectID = require ('mongodb').ObjectID; var db = null; @@ -71,11 +71,10 @@ module.exports = { db .collection ('skill_list') .update ({_id: ObjectID (id)}, skill, {upsert: true}, (err, result) => { - if (JSON.parse (result).nModified === 1) { - //database update ok - resolve (); - } else { - reject (); + if (err){ + reject(); + }else{ + resolve(); } }); }); @@ -86,8 +85,11 @@ module.exports = { db .collection ('skill_list') .remove ({_id: ObjectID (id)}, (err, result) => { - if (err) reject (err); - resolve (result); + if (err){ + reject (err); + }else{ + resolve (result); + } }); }); }, @@ -98,8 +100,11 @@ module.exports = { .collection ('skill_list') .find ({_id: ObjectID (id)}) .toArray ((err, result) => { - if (err) reject (err); - resolve (result); + if (err) { + reject (err); + }else{ + resolve (result); + } }); }); }, diff --git a/backend/server.js b/backend/server.js new file mode 100644 index 0000000..03df6f8 --- /dev/null +++ b/backend/server.js @@ -0,0 +1,122 @@ +var amazonHelper = require ('./helpers/amazon'); +var databaseHelper = require ('./helpers/database'); +const config = require ('./config/config'); +const constants = require ('./config/constants'); +require ('isomorphic-fetch'); +var express = require ('express'); +var alexa = require('./components/alexa'); +var bodyParser = require ('body-parser'); +var MongoClient = require ('mongodb').MongoClient; +var ObjectID = require ('mongodb').ObjectID; + +const router = express.Router (); +var app = express (); + +// ALWAYS setup the alexa app and attach it to express before anything else. +alexa.init(app); + +// from here on you can setup any other express routes or middlewares as nor + +app.set ('view engine', 'ejs'); + +/* +router.get ('/deleteSkill/:skillID', async (req, res, next) => { + databaseHelper + .deleteSkill (req.params.id) + .then (result => { + res.json (result); + }) + .catch (err => { + res.json (err); + }); +}); +*/ + +router.get ('/getSkill/:id', async (req, res, next) => { + const id = req.params.id; + + if (id.length !== constants.skillIDLength) { + res.json ([]); + } else { + databaseHelper + .getSkill (id) + .then (result => { + res.json (result); + }) + .catch (err => { + res.json ([]); + }); + } +}); + +router.post ('/updateSkill/:id', async (req, res, next) => { + let id = req.params.id; + let dataFromWeb = JSON.stringify(req.body); + let skill = JSON.parse(dataFromWeb); + let updateOnAmazon = skill.updateOnAmazon; + delete skill.updateOnAmazon; + delete skill._id; + console.log('id = ' + id); + if (id !== '-1') { + if (updateOnAmazon){ + amazonHelper.updateSkill(skill).then(amazonResult=>{ + console.log('Amazon : ' + amazonResult); + if (amazonResult === constants.amazonResultCodes.ok || amazonResult === constants.amazonResultCodes.accepted) { + //Skill uploaded, it's ok to update databaseI + databaseHelper + .updateSkill (id, skill) + .then (result => { + res.json ({result: constants.apiResultCodes.ok, message: ''}); + alexa.updateIntentsJSON (); + }) + .catch (e => { + res.json ({result: constants.apiResultCodes.databaseError, message: ''}); + }); + }else{ + res.json({result: constants.apiResultCodes.amazonError, message: amazonResult}); + } + }).catch(e=>{ + res.json ({result: constants.apiResultCodes.amazonError, message: 'unknown'}); + }); + }else{ + databaseHelper + .updateSkill (id, skill) + .then (result => { + res.json ({result: constants.apiResultCodes.ok, message: ''}); + alexa.updateIntentsJSON (); + }) + .catch (e => { + res.json ({result: constants.apiResultCodes.databaseError, message: ''}); + }); + } + + } else { + //no new skills for now + } +}); + +app.use (function (req, res, next) { + res.header ('Access-Control-Allow-Origin', '*'); + res.header ('Access-Control-Allow-Headers', 'Origin, Content-Type'); + res.header ('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); + res.header ('Access-Control-Allow-Credentials', 'true'); + next (); +}); + +app.use (bodyParser.json ()); +app.use ('/', router); + +MongoClient.connect (config.dbURL) + .then (database => { + databaseHelper.initModule (database); + + app.listen (config.PORT, () => { + console.log ('Express server running on port ' + config.PORT); + alexa.updateIntentsJSON (); + databaseHelper.loadTokens (); + amazonHelper.getStatus(config.SKILL_ID); + }); + }) + .catch (e => { + console.log ('error : ' + e); + }); diff --git a/web/README.md b/web/README.md deleted file mode 100644 index c13f2f0..0000000 --- a/web/README.md +++ /dev/null @@ -1,82 +0,0 @@ -To obtain new Auth Code : - -https://www.amazon.com/ap/oa?client_id=amzn1.application-oa2-client.c748ca56ded04a95b236979898585ff7&scope=alexa::ask:skills:readwrite alexa::ask:models:readwrite alexa::ask:skills:test&response_type=code&redirect_uri=https://layla.amazon.com/api/skill/link/M2ODJY6EXOY6KO - -Response URL (Decoded) : - -https://layla.amazon.com/api/skill/link/M2ODJY6EXOY6KO?code=ANCgZUfEFdlRRkpSNFuA&scope=alexa::ask:skills:readwrite alexa::ask:models:readwrite alexa::ask:skills:test - -Code : ANCgZUfEFdlRRkpSNFuA - -======================================= - -Now to get Access Token : - -Send a POST request to https://api.amazon.com/auth/o2/token with the following parameters - - - HTTP Header Parameters - - Content-Type: application/x-www-form-urlencoded - - - HTTP Body Parameters - - grant_type: authorization_code - code: The authorization code that was returned in the response. - client_id: The product’s client ID. To access this information, navigate to Amazon’s Developer Console. After you’ve logged in, click Get Started > under Alexa Voice Service, then click Edit next to a registered product (or create a new one). From the left navigation select Security Profile. This page contains both Client ID and Client Secret for your product. - client_secret: The product’s client secret. To access this information, navigate to Amazon’s Developer Console. After you’ve logged in, click Get Started > under Alexa Voice Service, then click Edit next to a registered product (or create a new one). From the left navigation select Security Profile. This page contains both Client ID and Client Secret for your product. - redirect_uri: One of the return URIs that you added to your app’s security profile when signing up. - -Response : - -{ - "access_token": "Atza|IwEBIBe6gDqrrowEEav6N-_6s4NztYeP3oG8PGWmu8ZiZw6lbOh3wNla3TK6pY-VEpT1d8an-dVf_n3kXJzVFsNo_4xBfZyFHGoCTDTFjs3yBRul4PVdBOhwwiH3-sgRLcUofZbe2oE06GmTcbfYtaStfXpQI5dfpldfnsJg_CvhSA6AHb_snJT3F6lyXzbV076d_3cYUMJxFldJGnYcviNHHxjjmuQTD06hhGzCbAxxe9eBmkuopRsNfyedLT2UlKP_ublah9CUGA3AdIX_3Iuke82jMwGnNl9gv7pbaDNEjAbj7IQSl3B08uuREtJq-oTBOjALNXRvFxTJmQjZwXNf9eHC7fSHJDdEPdZQU0AcffRQObAyAkUuL6Jv39OHzhb3Q64-zzoyODqnJyLP5SQZ2JVF53Kc_cTBqjIc9pXljqe7yEVk6JDs7q1zKbBibx_AQm57TO79IzWyLBzBMlYL5HdTsqEfRzLeDw2tws-hGMgkx2HWfdbYnmf5Qb4SyIhzvmmdfPLg3MVKTxjIBu1rx0xf3n0PLZP1EO6jsJPoMRPg77Gm4oit5Zp6s37ek3A3Vxh-ntoASpkrkxGTG9kVtRNt", - "refresh_token": "Atzr|IwEBICA3kDhfSJxlwvnQp9AD1o115AC_KBbFd5GBg8oN_QHWn2or5xFQ09BruuK6a07tGHtTt_9q2Y21mnOMH4RDtYXTVG9ADgLE6mHWKZFxYVwt3kHMiUJdY5lJcsOtWLoblrS-bJ0BEXXK5nVDt_bSI5IB7NUf-9QVZxhovRH_ANSxdTjJT0_rMIAZY3WEj68FEap49q_pg72BhnxHVZD2TC3zvX96_DN65HE5SoSgT7OiMAeiJewB_SyemW_HxBwaB-_X-G1ifOtnrzZ4gXTpOrEUlHI2YPuvRMBMtmz1h-nXDZYv3vwU3RA57Qj_ZNVcScj8_RXf2xq8w48v0PzZFXYBSalfnqPq6eUvSSbAJUp6bB8y596JlvR5dFQe_Z--X0Gkfo85IcyrI9D44vK9sJhrGhCVi2FDDa8pHczmNSen99JYZvDif-zpYzgbcymAkOV0gC7JvYMxlZfETT3NTBy7eVA7fJI1SZaeA_qW49xRcBkZBu5gkqTpeGWUU1cGr2aXRVVmXGM22NfV5E7KzvEBsCeHml_tCfxZeKY8Msd8hJb0Cd59u-_hsuc8oNjsOpIdFF976dY3uTmAgHWpG2PH", - "token_type": "bearer", - "expires_in": 3600 -} - -================================ - -Now to get new Access token using refresh token : - -Send a POST request to https://api.amazon.com/auth/o2/token with the following parameters: - - - HTTP Header Parameters - - Content-Type: application/x-www-form-urlencoded - - - HTTP Body Parameters - - grant_type: refresh_token - refresh_token: The URL-encoded refresh token returned with the last request for a new access token. - client_id: To access this information, navigate to Amazon’s Developer Console. After you’ve logged in, click Get Started > under Alexa Voice Service, then click Edit next to a registered product (or create a new one). From the left navigation select Security Profile. This page contains both Client ID and Client Secret for your product. - client_secret: The website’s client secret. To access this information, navigate to Amazon’s Developer Console. After you’ve logged in, click Get Started > under Alexa Voice Service, then click Edit next to a registered product (or create a new one). From the left navigation select Security Profile. This page contains both Client ID and Client Secret for your product. - -Response : - -{ - "access_token": "Atza|IwEBILtBe3hrHovrMx7Oivng-RB2EKzvCm_epXJE2HXPMQzXTFqK10Zrqt-Z8paeRoLQBqbLCmqWvcr5RTNgw9qjtfzOTsOrXC1VKqKmxpqHTrJyn2TLGsCzFjBDfADNjCyufWTf2ZlsSzjxW2GiqCHlwoPSd9pFrLavtRThrm1J-5KvnFrj-yD-tYTSwrgX5W5p2SrjQxoE3aP5b96z6p8GvCL9lM1pddafAxkHb22A3IzR-pYGmEijb4ksRuaIf4WCNwssWV6GBIB2oJA5CU-Dtd2mOZZ5-dYpSSeCHyGumTYecTxxMVSdiVjCqB8WT6AtvvutWFQQoldHjJmIwBsTZP-iQcl-UyajOZJ03GqRUym5Hp-49uByzVG-MfR_Z5qVmYjjsLQEOLCY9kPVnmRGnOTj6YPjrHXibd6P8TQOMh4VTcgFpg-afKKABP6EeDwok1t2ivuYh5OJju-B1A6gzhMi4vQJYKq107e0QMYBBhrf_OqCgMbfnQZ8j40qocVGID5YWv8uk5wKyI61LrbzrTltmzxzNemzqbSBzwAlfNS6GW-jVjg8svsi1lb_EVRbhyOoWJWX3mEd-5GDYyUcyInleiAR0aIHVP94pZxqdiCamA", - "refresh_token": "Atzr|IwEBICA3kDhfSJxlwvnQp9AD1o115AC_KBbFd5GBg8oN_QHWn2or5xFQ09BruuK6a07tGHtTt_9q2Y21mnOMH4RDtYXTVG9ADgLE6mHWKZFxYVwt3kHMiUJdY5lJcsOtWLoblrS-bJ0BEXXK5nVDt_bSI5IB7NUf-9QVZxhovRH_ANSxdTjJT0_rMIAZY3WEj68FEap49q_pg72BhnxHVZD2TC3zvX96_DN65HE5SoSgT7OiMAeiJewB_SyemW_HxBwaB-_X-G1ifOtnrzZ4gXTpOrEUlHI2YPuvRMBMtmz1h-nXDZYv3vwU3RA57Qj_ZNVcScj8_RXf2xq8w48v0PzZFXYBSalfnqPq6eUvSSbAJUp6bB8y596JlvR5dFQe_Z--X0Gkfo85IcyrI9D44vK9sJhrGhCVi2FDDa8pHczmNSen99JYZvDif-zpYzgbcymAkOV0gC7JvYMxlZfETT3NTBy7eVA7fJI1SZaeA_qW49xRcBkZBu5gkqTpeGWUU1cGr2aXRVVmXGM22NfV5E7KzvEBsCeHml_tCfxZeKY8Msd8hJb0Cd59u-_hsuc8oNjsOpIdFF976dY3uTmAgHWpG2PH", - "token_type": "bearer", - "expires_in": 3600 -} - -======================= -======================= - -Prerequests for step 3 : - - Database (tellall) with collection (skill_list) - Insert dummy skill with : db.skill_list.insert({"skillID" : "amzn1.ask.skill.efbf0564-a732-4ba9-958f-57939138adae", "intents" : [ { "intentName" : "GetFirstQuestion", "questions" : [ "tell me something about projects", "tell me all about projects" ], "answer" : "blablabla bla bla" }, { "intentName" : "GetThirdQuestion", "questions" : [ "Give me third question" ], "answer" : "This is answer to the third question" } ], "invocationName" : "Saburly", "invocationAnswer" : "We are Saburly team one" }) - obtain _id and change in web/src/App.js, and also skill_db_id in backend/config.js - enter web/ dir and run "npm run build" - - Database (tellall) with collection (token_list) - Insert tokens with : db.token_list.insert({"id" : 1, "refresh_token" : "...", "access_token" : "...", "expires_in" : 1515173601.754 }) - (Change refresh_token and access_token dots with real ones) - - Set skill_id, client_id and client_secret to appropriate values in backend/config.js - Set base_url to "tellall.saburly.com" in web/src/config. - - Start backend service from backend/ running "node express.js" - diff --git a/web/src/App.js b/web/src/App.js index 63aa044..2d405e2 100644 --- a/web/src/App.js +++ b/web/src/App.js @@ -10,7 +10,8 @@ import {getSkill, updateSkill} from './lib/api' import { NEW_INTENT_SELECTED_INDEX, LAUNCH_REQUEST_SELECTED_INDEX, - CONTACT_SELECTED_INDEX} from './config' + CONTACT_SELECTED_INDEX, + RESULT_CODES} from './config/constants' class App extends Component { @@ -180,8 +181,8 @@ class App extends Component { return new Promise((resolve,reject)=>{ updateSkill(this.createSkill(newAllIntents,newName,newAnswer,email,updateOnAmazon)).then(l=>l.text()).then(result=>{ let jResult = JSON.parse(result); - if (jResult.result !== 0){ - console.log(jResult.result); + if (jResult.result !== RESULT_CODES.OK){ + console.log(jResult.result); if (showPopUp) Popup.alert('Model was not saved. Please try again'); this.setState(rejectState); //reject('Error code : ' + jResult.result); @@ -191,7 +192,7 @@ class App extends Component { resolve(); } }).catch(e=>{ - console.log('error : ' + e); + console.log('error : ' + e); if (showPopUp) Popup.alert('Model was not saved. Please try again'); this.setState(rejectState); //reject(e); diff --git a/web/src/components/Contact.js b/web/src/components/Contact.js index 31e6b60..8a0efa8 100644 --- a/web/src/components/Contact.js +++ b/web/src/components/Contact.js @@ -1,7 +1,7 @@ import React, { Component } from 'react'; import {Button, TextField} from 'react-md'; import '../css/Intent.css' -import {EMAIL_MAX_LENGTH} from '../config'; +import {EMAIL_MAX_LENGTH} from '../config/constants'; class Contact extends Component { constructor(props){ diff --git a/web/src/components/IntentDetails.js b/web/src/components/IntentDetails.js index 3919e99..de1c4a0 100644 --- a/web/src/components/IntentDetails.js +++ b/web/src/components/IntentDetails.js @@ -1,7 +1,7 @@ import React, { Component } from 'react'; import {Button, SVGIcon, TextField} from 'react-md'; import '../css/Intent.css' -import {QUESTION_MAX_LENGTH, ANSWER_MAX_LENGTH, INTENT_NAME_MAX_LENGTH} from '../config'; +import {QUESTION_MAX_LENGTH, ANSWER_MAX_LENGTH, INTENT_NAME_MAX_LENGTH} from '../config/constants'; class IntentDetails extends Component { constructor(props){ diff --git a/web/src/components/IntentItem.js b/web/src/components/IntentItem.js index 0aa29a5..124d7fd 100644 --- a/web/src/components/IntentItem.js +++ b/web/src/components/IntentItem.js @@ -1,7 +1,7 @@ import React, { Component } from 'react'; import {Button} from 'react-md'; import '../css/Intent.css' -import {INTENT_TITLE_MAX_LENGTH, INTENT_TITLE_TOOLTIP_DELAY} from '../config' +import {INTENT_TITLE_MAX_LENGTH, INTENT_TITLE_TOOLTIP_DELAY} from '../config/constants' class IntentItem extends Component { diff --git a/web/src/components/IntentList.js b/web/src/components/IntentList.js index e77032a..6722fc0 100644 --- a/web/src/components/IntentList.js +++ b/web/src/components/IntentList.js @@ -4,7 +4,7 @@ import IntentItem from './IntentItem'; import '../css/Intent.css' import { LAUNCH_REQUEST_SELECTED_INDEX, - CONTACT_SELECTED_INDEX} from '../config' + CONTACT_SELECTED_INDEX} from '../config/constants' class IntentList extends Component { constructor (props){ diff --git a/web/src/components/LaunchRequest.js b/web/src/components/LaunchRequest.js index c713aa3..c6c1b33 100644 --- a/web/src/components/LaunchRequest.js +++ b/web/src/components/LaunchRequest.js @@ -1,7 +1,7 @@ import React, { Component } from 'react'; import {Button, TextField} from 'react-md'; import '../css/Intent.css' -import { INVOCATION_NAME_MAX_LENGTH, INVOCATION_ANSWER_MAX_LENGTH } from '../config'; +import { INVOCATION_NAME_MAX_LENGTH, INVOCATION_ANSWER_MAX_LENGTH } from '../config/constants'; class LaunchRequest extends Component { constructor(props){ diff --git a/web/src/config/config.js b/web/src/config/config.js new file mode 100644 index 0000000..ab2244e --- /dev/null +++ b/web/src/config/config.js @@ -0,0 +1,3 @@ +export const BASE_URL = 'tellall.saburly.com'; + + diff --git a/web/src/config.js b/web/src/config/constants.js similarity index 89% rename from web/src/config.js rename to web/src/config/constants.js index b7d4e2e..dc122ff 100644 --- a/web/src/config.js +++ b/web/src/config/constants.js @@ -1,5 +1,3 @@ -export const BASE_URL = 'tellall.saburly.com'; - export const INTENT_NAME_MAX_LENGTH = 30; export const QUESTION_MAX_LENGTH = 150; export const ANSWER_MAX_LENGTH = 150; @@ -15,3 +13,8 @@ export const EMAIL_MAX_LENGTH = 100; export const NEW_INTENT_SELECTED_INDEX = -1; export const LAUNCH_REQUEST_SELECTED_INDEX = -2; export const CONTACT_SELECTED_INDEX = -3; + +export const RESULT_CODES = { + OK:0, + ERROR:-1 +} \ No newline at end of file diff --git a/web/src/lib/api.js b/web/src/lib/api.js index 229cbf3..d55c12c 100644 --- a/web/src/lib/api.js +++ b/web/src/lib/api.js @@ -1,5 +1,5 @@ import fetch from 'isomorphic-fetch'; -import {BASE_URL} from '../config'; +import {BASE_URL} from '../config/config'; export const getAllIntents = (id)=>{ let url = `http://${BASE_URL}/intents/${id}` diff --git a/web/src/logo.svg b/web/src/logo.svg deleted file mode 100644 index 6b60c10..0000000 --- a/web/src/logo.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - From bbec4e9940048551463bd6cc655d5601726de47f Mon Sep 17 00:00:00 2001 From: GotPPay Date: Fri, 12 Jan 2018 02:12:05 +0100 Subject: [PATCH 32/83] . --- backend/server.js | 1 - 1 file changed, 1 deletion(-) diff --git a/backend/server.js b/backend/server.js index 03df6f8..57b2d2c 100644 --- a/backend/server.js +++ b/backend/server.js @@ -114,7 +114,6 @@ MongoClient.connect (config.dbURL) console.log ('Express server running on port ' + config.PORT); alexa.updateIntentsJSON (); databaseHelper.loadTokens (); - amazonHelper.getStatus(config.SKILL_ID); }); }) .catch (e => { From 7f56a2850966b807770195422e324acf81047d81 Mon Sep 17 00:00:00 2001 From: GotPPay Date: Sat, 13 Jan 2018 14:57:41 +0100 Subject: [PATCH 33/83] backend code refactoring --- backend/components/alexa.js | 7 ++- backend/config/config.js | 4 +- backend/controllers/index.js | 6 +++ backend/controllers/skill.js | 91 +++++++++++++++++++++++++++++++++ backend/helpers/amazon.js | 69 ++++++++++++------------- backend/middleware/index.js | 7 +++ backend/server.js | 98 +++--------------------------------- 7 files changed, 147 insertions(+), 135 deletions(-) create mode 100644 backend/controllers/index.js create mode 100644 backend/controllers/skill.js create mode 100644 backend/middleware/index.js diff --git a/backend/components/alexa.js b/backend/components/alexa.js index 93bbcc2..b06c44f 100644 --- a/backend/components/alexa.js +++ b/backend/components/alexa.js @@ -17,19 +17,18 @@ var alexaApp = new alexa.app ('saburly'); // this means we still work with one s module.exports = { init: function (express) { - alexaApp.express ({ expressApp: express, - + // verifies requests come from amazon alexa. Must be enabled for production. // You can disable this if you're running a dev environment and want to POST // things to test behavior. enabled by default. checkCert: false, - + // sets up a GET route when set to true. This is handy for testing in // development, but not recommended for production. disabled by default debug: true, - }); + }); }, updateIntentsJSON: function () { diff --git a/backend/config/config.js b/backend/config/config.js index 5c41336..3edee7d 100644 --- a/backend/config/config.js +++ b/backend/config/config.js @@ -8,8 +8,8 @@ config.REFRESH_TOKEN = 'Atzr|IwEBICA3kDhfSJxlwvnQp9AD1o115AC_KBbFd5GBg8oN_QHWn2o config.TOKEN_EXPIRES_IN = 1515100500; config.SKILL_ID = 'amzn1.ask.skill.efbf0564-a732-4ba9-958f-57939138adae'; -config.SKILL_DB_ID = '5a5016e775becaef2015da10'; //for server -//config.SKILL_DB_ID = '5a232fb86ce046c749739455'; //for local +//config.SKILL_DB_ID = '5a5016e775becaef2015da10'; //for server +config.SKILL_DB_ID = '5a232fb86ce046c749739455'; //for local config.CLIENT_ID = 'amzn1.application-oa2-client.c748ca56ded04a95b236979898585ff7'; config.CLIENT_SECRET = '6dea8125cecd049d3c4cff7bb5bdfd3ff17bc6fed246c4c8f6b519d9ed08d0b3'; diff --git a/backend/controllers/index.js b/backend/controllers/index.js new file mode 100644 index 0000000..9cf449c --- /dev/null +++ b/backend/controllers/index.js @@ -0,0 +1,6 @@ +var express = require ('express'), router = express.Router (); + +router.use ('/getSkill', require ('./skill')); +router.use ('/updateSkill', require ('./skill')); // this will be fixed in next code refactoring + +module.exports = router; diff --git a/backend/controllers/skill.js b/backend/controllers/skill.js new file mode 100644 index 0000000..4a5fc62 --- /dev/null +++ b/backend/controllers/skill.js @@ -0,0 +1,91 @@ +var express = require ('express'), router = express.Router (); +const constants = require ('../config/constants'); +var databaseHelper = require ('../helpers/database'); +var amazonHelper = require ('../helpers/amazon'); +var bodyParser = require ('body-parser'); +var alexa = require ('../components/alexa'); + +router.get ('/:id', async (req, res, next) => { + const id = req.params.id; + + if (id.length !== constants.skillIDLength) { + res.json ([]); + } else { + databaseHelper + .getSkill (id) + .then (result => { + res.json (result); + }) + .catch (err => { + res.json ([]); + }); + } +}); + +router.post ('/:id', bodyParser.json (), async (req, res, next) => { + let id = req.params.id; + let dataFromWeb = JSON.stringify (req.body); + let skill = JSON.parse (dataFromWeb); + let updateOnAmazon = skill.updateOnAmazon; + + delete skill.updateOnAmazon; + delete skill._id; + + console.log ('id = ' + id); + + if (id !== '-1') { + if (updateOnAmazon) { + amazonHelper + .updateSkill (skill) + .then (amazonResult => { + console.log ('Amazon : ' + amazonResult); + if ( + amazonResult === constants.amazonResultCodes.ok || + amazonResult === constants.amazonResultCodes.accepted + ) { + //Skill uploaded, it's ok to update databaseI + databaseHelper + .updateSkill (id, skill) + .then (result => { + res.json ({result: constants.apiResultCodes.ok, message: ''}); + alexa.updateIntentsJSON (); + }) + .catch (e => { + res.json ({ + result: constants.apiResultCodes.databaseError, + message: '', + }); + }); + } else { + res.json ({ + result: constants.apiResultCodes.amazonError, + message: amazonResult, + }); + } + }) + .catch (e => { + res.json ({ + result: constants.apiResultCodes.amazonError, + message: 'unknown', + }); + }); + } else { + databaseHelper + .updateSkill (id, skill) + .then (result => { + res.json ({result: constants.apiResultCodes.ok, message: ''}); + alexa.updateIntentsJSON (); + }) + .catch (e => { + res.json ({ + result: constants.apiResultCodes.databaseError, + message: '', + }); + }); + } + } else { + //no new skills for now + } +}); + +module.exports = router; diff --git a/backend/helpers/amazon.js b/backend/helpers/amazon.js index ab31000..3a66cbf 100644 --- a/backend/helpers/amazon.js +++ b/backend/helpers/amazon.js @@ -5,18 +5,16 @@ var databaseHelper = require ('./database'); var getBuildStatus = function (skillID) { fetch ( - `https://api.amazonalexa.com/v0/skills/${skillID}/interactionModel/locales/en-US/status`, - { - method: 'GET', - headers: { - Authorization: config.TOKEN, - }, - } - ) - .then (l => l.text ()) - .then (result => { - return result; - }); + `https://api.amazonalexa.com/v0/skills/${skillID}/interactionModel/locales/en-US/status`, + { + method: 'GET', + headers: { + Authorization: config.TOKEN, + }, + } + ).then (result => { + return result.text (); + }); }; var refreshTokens = function () { @@ -45,9 +43,9 @@ var refreshTokens = function () { parsedResponse.access_token, parsedResponse.expires_in ); - console.log('Token refresh failed'); - console.log(body); - reject(body); + console.log ('Token refresh failed'); + console.log (body); + reject (body); }); }); }; @@ -80,52 +78,49 @@ var generateInteractionModel = function (skill) { allIntents.push ({name: intent.intentName, samples: intent.questions}); }); - //Special Email Intents : - allIntents.push({ + allIntents.push ({ name: 'EmailIntentLaunch', - slots:[], + slots: [], samples: [ 'I want to send a message', 'I would like to send a message', 'I would like to leave a message', - 'Leave a message' - ] + 'Leave a message', + ], }); - allIntents.push({ + allIntents.push ({ name: 'EmailIntent', - slots:[ + slots: [ { - 'name': 'Name', - 'type': 'AMAZON.US_FIRST_NAME' + name: 'Name', + type: 'AMAZON.US_FIRST_NAME', }, { - 'name': 'Email', - 'type': 'AMAZON.LITERAL' + name: 'Email', + type: 'AMAZON.LITERAL', }, { - 'name': 'Message', - 'type': 'AMAZON.LITERAL' + name: 'Message', + type: 'AMAZON.LITERAL', }, { - 'name': 'Data', - 'type': 'AMAZON.LITERAL' - } + name: 'Data', + type: 'AMAZON.LITERAL', + }, ], - - + samples: [ 'My name is {Name}', 'I am {Name}', '{exampleww at wwdwdw|Data}', 'My email is {example at efefegedd|Email}', 'Send replay to {example at abcdefg|Email}', - 'My message is {The quick brown fox jumps over the lazy dog.The quick brown fox jumps over the lazy dog.The quick brown fox jumps over the lazy dog.|Message}' - ] - + 'My message is {The quick brown fox jumps over the lazy dog.The quick brown fox jumps over the lazy dog.The quick brown fox jumps over the lazy dog.|Message}', + ], }); - + result.interactionModel = {}; result.interactionModel.languageModel = { diff --git a/backend/middleware/index.js b/backend/middleware/index.js new file mode 100644 index 0000000..0b842fa --- /dev/null +++ b/backend/middleware/index.js @@ -0,0 +1,7 @@ +module.exports = function (req, res, next) { + res.header ('Access-Control-Allow-Origin', '*'); + res.header ('Access-Control-Allow-Headers', 'Origin, Content-Type'); + res.header ('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); + res.header ('Access-Control-Allow-Credentials', 'true'); + next (); +}; diff --git a/backend/server.js b/backend/server.js index 57b2d2c..cbed3c5 100644 --- a/backend/server.js +++ b/backend/server.js @@ -5,7 +5,8 @@ const constants = require ('./config/constants'); require ('isomorphic-fetch'); var express = require ('express'); var alexa = require('./components/alexa'); -var bodyParser = require ('body-parser'); + + var MongoClient = require ('mongodb').MongoClient; var ObjectID = require ('mongodb').ObjectID; @@ -13,105 +14,18 @@ const router = express.Router (); var app = express (); // ALWAYS setup the alexa app and attach it to express before anything else. -alexa.init(app); - -// from here on you can setup any other express routes or middlewares as nor +alexa.init (app); app.set ('view engine', 'ejs'); - -/* -router.get ('/deleteSkill/:skillID', async (req, res, next) => { - databaseHelper - .deleteSkill (req.params.id) - .then (result => { - res.json (result); - }) - .catch (err => { - res.json (err); - }); -}); -*/ - -router.get ('/getSkill/:id', async (req, res, next) => { - const id = req.params.id; - - if (id.length !== constants.skillIDLength) { - res.json ([]); - } else { - databaseHelper - .getSkill (id) - .then (result => { - res.json (result); - }) - .catch (err => { - res.json ([]); - }); - } -}); - -router.post ('/updateSkill/:id', async (req, res, next) => { - let id = req.params.id; - let dataFromWeb = JSON.stringify(req.body); - let skill = JSON.parse(dataFromWeb); - let updateOnAmazon = skill.updateOnAmazon; - delete skill.updateOnAmazon; - delete skill._id; - console.log('id = ' + id); - if (id !== '-1') { - if (updateOnAmazon){ - amazonHelper.updateSkill(skill).then(amazonResult=>{ - console.log('Amazon : ' + amazonResult); - if (amazonResult === constants.amazonResultCodes.ok || amazonResult === constants.amazonResultCodes.accepted) { - //Skill uploaded, it's ok to update databaseI - databaseHelper - .updateSkill (id, skill) - .then (result => { - res.json ({result: constants.apiResultCodes.ok, message: ''}); - alexa.updateIntentsJSON (); - }) - .catch (e => { - res.json ({result: constants.apiResultCodes.databaseError, message: ''}); - }); - }else{ - res.json({result: constants.apiResultCodes.amazonError, message: amazonResult}); - } - }).catch(e=>{ - res.json ({result: constants.apiResultCodes.amazonError, message: 'unknown'}); - }); - }else{ - databaseHelper - .updateSkill (id, skill) - .then (result => { - res.json ({result: constants.apiResultCodes.ok, message: ''}); - alexa.updateIntentsJSON (); - }) - .catch (e => { - res.json ({result: constants.apiResultCodes.databaseError, message: ''}); - }); - } - - } else { - //no new skills for now - } -}); - -app.use (function (req, res, next) { - res.header ('Access-Control-Allow-Origin', '*'); - res.header ('Access-Control-Allow-Headers', 'Origin, Content-Type'); - res.header ('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); - res.header ('Access-Control-Allow-Credentials', 'true'); - next (); -}); - -app.use (bodyParser.json ()); -app.use ('/', router); +app.use (require ('./middleware')); //common middleware for all requests +app.use (require ('./controllers')); //all routes MongoClient.connect (config.dbURL) .then (database => { databaseHelper.initModule (database); app.listen (config.PORT, () => { - console.log ('Express server running on port ' + config.PORT); + console.log ('Express server running on port ' + config.PORT); alexa.updateIntentsJSON (); databaseHelper.loadTokens (); }); From 07857fd2f4ad5e2ccbfd9a365abe210bcea47dcb Mon Sep 17 00:00:00 2001 From: GotPPay Date: Sun, 14 Jan 2018 01:00:35 +0100 Subject: [PATCH 34/83] frontend code refactoring --- web/src/App.js | 67 +++++++------- web/src/components/Contact.js | 12 +-- web/src/components/IntentDetails.js | 22 ++--- web/src/components/IntentItem.js | 2 +- web/src/components/IntentList.js | 8 +- web/src/components/LaunchRequest.js | 17 ++-- web/src/config/config.js | 3 +- web/src/config/constants.js | 6 +- web/src/css/Common.scss | 21 +++++ web/src/css/Intent.css | 82 ------------------ web/src/css/Intent.scss | 101 ---------------------- web/src/css/components/Contact.scss | 4 + web/src/css/components/IntentDetails.scss | 18 ++++ web/src/css/components/IntentItem.scss | 18 ++++ web/src/css/components/IntentList.scss | 52 +++++++++++ web/src/css/components/LaunchRequest.scss | 11 +++ 16 files changed, 186 insertions(+), 258 deletions(-) create mode 100644 web/src/css/Common.scss delete mode 100644 web/src/css/Intent.css delete mode 100644 web/src/css/Intent.scss create mode 100644 web/src/css/components/Contact.scss create mode 100644 web/src/css/components/IntentDetails.scss create mode 100644 web/src/css/components/IntentItem.scss create mode 100644 web/src/css/components/IntentList.scss create mode 100644 web/src/css/components/LaunchRequest.scss diff --git a/web/src/App.js b/web/src/App.js index 2d405e2..571797a 100644 --- a/web/src/App.js +++ b/web/src/App.js @@ -18,7 +18,7 @@ class App extends Component { constructor(props){ super(props); - this.state={_id:'5a5016e775becaef2015da10', + this.state={_id:'5a232fb86ce046c749739455', skillID:'', skillName:'', invocationName:'Saburly', @@ -30,8 +30,8 @@ class App extends Component { waiting: false }; - getSkill(this.state._id).then(l=> l.text()).then(result=>{ - let jResult = JSON.parse(result)[0]; + getSkill(this.state._id).then(l=>l.json()).then(result=>{ + let jResult = result[0]; if (jResult===undefined) return; this.setState({ skillID:jResult.skillID,skillName:jResult.skillName, invocationName: jResult.invocationName, invocationAnswer: jResult.invocationAnswer, @@ -51,6 +51,26 @@ class App extends Component { } render() { + let rightPanel; + switch (this.state.selectedIndex) { + case LAUNCH_REQUEST_SELECTED_INDEX: + rightPanel = ; + break; + case CONTACT_SELECTED_INDEX: + rightPanel = ; + break; + default: + rightPanel = ; + } + return(
@@ -63,37 +83,9 @@ class App extends Component { onIntentClick={this.handleIntentClick} onAddIntentClick={this.handleAddIntentClick} selectedIndex={this.state.selectedIndex} - waiting={this.state.waiting}> - - {( - ()=>{ - if (this.state.selectedIndex===LAUNCH_REQUEST_SELECTED_INDEX){ - return ( - - - ); - }else if (this.state.selectedIndex===CONTACT_SELECTED_INDEX){ - return ( - - - ); - }else{ - return( - - - ); - } - } - )()} + waiting={this.state.waiting}/> + + {rightPanel}
); } @@ -179,10 +171,9 @@ class App extends Component { sendSkill(newAllIntents, showPopUp, resolveState, rejectState, newName, newAnswer, email, updateOnAmazon){ return new Promise((resolve,reject)=>{ - updateSkill(this.createSkill(newAllIntents,newName,newAnswer,email,updateOnAmazon)).then(l=>l.text()).then(result=>{ - let jResult = JSON.parse(result); - if (jResult.result !== RESULT_CODES.OK){ - console.log(jResult.result); + updateSkill(this.createSkill(newAllIntents,newName,newAnswer,email,updateOnAmazon)).then(l=>l.json()).then(result=>{ + if (result.result !== RESULT_CODES.OK){ + console.log(result.result); if (showPopUp) Popup.alert('Model was not saved. Please try again'); this.setState(rejectState); //reject('Error code : ' + jResult.result); diff --git a/web/src/components/Contact.js b/web/src/components/Contact.js index 8a0efa8..669945b 100644 --- a/web/src/components/Contact.js +++ b/web/src/components/Contact.js @@ -1,6 +1,7 @@ import React, { Component } from 'react'; import {Button, TextField} from 'react-md'; -import '../css/Intent.css' +import '../css/Common.css'; +import '../css/components/Contact.css'; import {EMAIL_MAX_LENGTH} from '../config/constants'; class Contact extends Component { @@ -18,21 +19,20 @@ class Contact extends Component { render() { return ( -
-
Contact address will be used for direct messaging through Alexa
+
+
Contact address will be used for direct messaging through Alexa






-
diff --git a/web/src/components/IntentDetails.js b/web/src/components/IntentDetails.js index de1c4a0..5af5471 100644 --- a/web/src/components/IntentDetails.js +++ b/web/src/components/IntentDetails.js @@ -1,6 +1,6 @@ import React, { Component } from 'react'; import {Button, SVGIcon, TextField} from 'react-md'; -import '../css/Intent.css' +import '../css/components/IntentDetails.css'; import {QUESTION_MAX_LENGTH, ANSWER_MAX_LENGTH, INTENT_NAME_MAX_LENGTH} from '../config/constants'; class IntentDetails extends Component { @@ -22,20 +22,19 @@ class IntentDetails extends Component { render() { return ( -
+
-
Questions
+
Questions
{ this.state.intent.questions.map((question, index)=>{ return ( @@ -44,8 +43,7 @@ class IntentDetails extends Component { id="intent question" lineDirection="center" placeholder="Question" - className="md-cell md-cell--bottom" - style={{width:'60%'}} + className="md-cell md-cell--bottom IntentDetailsInputBoxes" maxLength={QUESTION_MAX_LENGTH} rightIcon={{this.deleteQuestion(question)}}> } onChange={(e)=>{this.handleQuestionEdit(e,index)}} @@ -62,18 +60,17 @@ class IntentDetails extends Component { lineDirection="center" label="Answer" placeholder="Answer" - style={{width:'60%'}} maxLength={ANSWER_MAX_LENGTH} - className="md-cell md-cell--bottom" + className="md-cell md-cell--bottom IntentDetailsInputBoxes" onChange={this.handleAnswerEdit} value={this.state.intent.answer}/>
}



- - - + + +
); @@ -110,7 +107,6 @@ class IntentDetails extends Component { handleIntentNameEdit(e){ if (e.length === INTENT_NAME_MAX_LENGTH) return; - //e.replace(/\s/g,''); let newIntent = this.state.intent; newIntent.intentName = e; this.setState({intent: newIntent}); diff --git a/web/src/components/IntentItem.js b/web/src/components/IntentItem.js index 124d7fd..ede0eca 100644 --- a/web/src/components/IntentItem.js +++ b/web/src/components/IntentItem.js @@ -1,6 +1,6 @@ import React, { Component } from 'react'; import {Button} from 'react-md'; -import '../css/Intent.css' +import '../css/components/IntentItem.css' import {INTENT_TITLE_MAX_LENGTH, INTENT_TITLE_TOOLTIP_DELAY} from '../config/constants' class IntentItem extends Component { diff --git a/web/src/components/IntentList.js b/web/src/components/IntentList.js index 6722fc0..96a86d1 100644 --- a/web/src/components/IntentList.js +++ b/web/src/components/IntentList.js @@ -1,7 +1,7 @@ import React, { Component } from 'react'; import {Button} from 'react-md'; import IntentItem from './IntentItem'; -import '../css/Intent.css' +import '../css/components/IntentList.css'; import { LAUNCH_REQUEST_SELECTED_INDEX, CONTACT_SELECTED_INDEX} from '../config/constants' @@ -20,11 +20,11 @@ class IntentList extends Component { render() { return (
- - @@ -43,7 +43,7 @@ class IntentList extends Component { }) }

-
diff --git a/web/src/components/LaunchRequest.js b/web/src/components/LaunchRequest.js index c6c1b33..a6ef19a 100644 --- a/web/src/components/LaunchRequest.js +++ b/web/src/components/LaunchRequest.js @@ -1,6 +1,7 @@ import React, { Component } from 'react'; import {Button, TextField} from 'react-md'; -import '../css/Intent.css' +import '../css/Common.css'; +import '../css/components/LaunchRequest.css'; import { INVOCATION_NAME_MAX_LENGTH, INVOCATION_ANSWER_MAX_LENGTH } from '../config/constants'; class LaunchRequest extends Component { @@ -19,33 +20,31 @@ class LaunchRequest extends Component { render() { return ( -
-
Invocation name customers use to activate the skill. For example "Open Saburly" or "Talk to Saburly"
+
+
Invocation name customers use to activate the skill. For example "Open Saburly" or "Talk to Saburly"


-
Answer customers get from Alexa when they activate the skill.
+
Answer customers get from Alexa when they activate the skill.




-
diff --git a/web/src/config/config.js b/web/src/config/config.js index ab2244e..4a3731b 100644 --- a/web/src/config/config.js +++ b/web/src/config/config.js @@ -1,3 +1,4 @@ -export const BASE_URL = 'tellall.saburly.com'; +//export const BASE_URL = 'tellall.saburly.com'; //for server +export const BASE_URL = 'localhost:5000'; //for local diff --git a/web/src/config/constants.js b/web/src/config/constants.js index 88e45c5..f4add8f 100644 --- a/web/src/config/constants.js +++ b/web/src/config/constants.js @@ -15,6 +15,6 @@ export const LAUNCH_REQUEST_SELECTED_INDEX = -2; export const CONTACT_SELECTED_INDEX = -3; export const RESULT_CODES = { - OK:0, - ERROR:-1 -} + OK: 0, + ERROR: -1, +}; diff --git a/web/src/css/Common.scss b/web/src/css/Common.scss new file mode 100644 index 0000000..80a7476 --- /dev/null +++ b/web/src/css/Common.scss @@ -0,0 +1,21 @@ +$minHeight : calc(100vh - 80px); //80px is height of the title div container + +/* Common for right panel components */ + +.RightPanelBox{ + float: left; + width: 70%; + min-height:$minHeight; + background-color: #f5f5f5; +} + +.PanelSubTitle{ + text-align: left; + margin-top: 30px; + margin-left: 20px; +} + +.SaveButton{ + float: right; + margin-right: 20px; +} \ No newline at end of file diff --git a/web/src/css/Intent.css b/web/src/css/Intent.css deleted file mode 100644 index bd94c64..0000000 --- a/web/src/css/Intent.css +++ /dev/null @@ -1,82 +0,0 @@ -/*IntentList and IntentItem CSS*/ -.IntentList { - width: 30%; - min-height: calc(100vh - 80px); - float: left; - background-color: #eff0f0; } - -.IntentList-title { - font-size: 1.5em; - height: 70px; - padding: 20px; - text-align: left; - background-color: #eff0f0; } - -.LaunchRequest { - text-align: left; - width: 100%; - height: 50px; - background-color: #d8d8d8; } - -.LaunchRequest-selected { - text-align: left; - width: 100%; - height: 50px; - background-color: #f5f5f5; } - -.Contact { - text-align: left; - color: #009b8a; - width: 100%; - height: 50px; - background-color: #d8d8d8; } - -.Contact-selected { - text-align: left; - color: #009b8a; - width: 100%; - height: 50px; - background-color: #f5f5f5; } - -.AddIntent { - float: right; - margin: 12px; } - -.IntentItem { - margin-top: 2px; - margin-bottom: 2px; - height: 50px; - width: 100%; - text-align: left; - background-color: #d8d8d8; } - -.IntentItem-selected { - margin-top: 2px; - margin-bottom: 2px; - height: 50px; - width: 100%; - text-align: left; - background-color: #f5f5f5; } - -/*IntentDetails CSS*/ -.IntentDetails { - float: left; - width: 70%; - min-height: calc(100vh - 80px); - background-color: #f5f5f5; } - -.QuestionBox { - margin: 25px; } - -/*LaunchRequest CSS*/ -.LaunchRequestBox { - float: left; - width: 70%; - min-height: calc(100vh - 80px); - background-color: #f5f5f5; } - -.ExplanationText { - float: left; - margin-top: 30px; - margin-left: 20px; - text-align: left; } diff --git a/web/src/css/Intent.scss b/web/src/css/Intent.scss deleted file mode 100644 index 3b35a99..0000000 --- a/web/src/css/Intent.scss +++ /dev/null @@ -1,101 +0,0 @@ -$minHeight : calc(100vh - 80px); - -/*IntentList and IntentItem CSS*/ - -.IntentList{ - width: 30%; - min-height:$minHeight; - float:left; - background-color: #eff0f0; -} - -.IntentList-title{ - font-size: 1.5em; - height: 70px; - padding: 20px; - text-align:left; - background-color: #eff0f0; -} - -.LaunchRequest{ - text-align: left; - width: 100%; - height: 50px; - background-color: #d8d8d8 -} - -.LaunchRequest-selected{ - text-align: left; - width: 100%; - height: 50px; - background-color: #f5f5f5; -} - -.Contact{ - text-align: left; - color: #009b8a; - width: 100%; - height: 50px; - background-color: #d8d8d8 -} - -.Contact-selected{ - text-align: left; - color: #009b8a; - width: 100%; - height: 50px; - background-color: #f5f5f5; -} - -.AddIntent{ - float: right; - margin: 12px; -} - -.IntentItem{ - margin-top: 2px; - margin-bottom: 2px; - height: 50px; - width:100%; - text-align:left; - background-color: #d8d8d8; -} - -.IntentItem-selected{ - margin-top: 2px; - margin-bottom: 2px; - height: 50px; - width:100%; - text-align:left; - background-color: #f5f5f5; - -} - -/*IntentDetails CSS*/ - -.IntentDetails{ - float: left; - width: 70%; - min-height:$minHeight; - background-color: #f5f5f5; -} - -.QuestionBox{ - margin:25px; -} - -/*LaunchRequest CSS*/ - -.LaunchRequestBox{ - float: left; - width:70%; - min-height:$minHeight; - background-color: #f5f5f5; -} - -.ExplanationText{ - float: left; - margin-top: 30px; - margin-left: 20px; - text-align: left; -} \ No newline at end of file diff --git a/web/src/css/components/Contact.scss b/web/src/css/components/Contact.scss new file mode 100644 index 0000000..10ce88b --- /dev/null +++ b/web/src/css/components/Contact.scss @@ -0,0 +1,4 @@ +.ContactEmailInput{ + width: 60%; + margin-left: 20px; +} diff --git a/web/src/css/components/IntentDetails.scss b/web/src/css/components/IntentDetails.scss new file mode 100644 index 0000000..8c65d70 --- /dev/null +++ b/web/src/css/components/IntentDetails.scss @@ -0,0 +1,18 @@ +.QuestionBox{ + margin:25px; +} + +.QuestionTitle{ + margin-top:20px; + margin-left: 30px; + float: left; +} + +.IntentDetailsInputBoxes{ + width: 60%; +} + +.IntentDetailsButton{ + float: left; + margin-left: 25px; +} \ No newline at end of file diff --git a/web/src/css/components/IntentItem.scss b/web/src/css/components/IntentItem.scss new file mode 100644 index 0000000..c729317 --- /dev/null +++ b/web/src/css/components/IntentItem.scss @@ -0,0 +1,18 @@ +.IntentItem{ + margin-top: 2px; + margin-bottom: 2px; + height: 50px; + width:100%; + text-align:left; + background-color: #d8d8d8; +} + +.IntentItem-selected{ + margin-top: 2px; + margin-bottom: 2px; + height: 50px; + width:100%; + text-align:left; + background-color: #f5f5f5; + +} diff --git a/web/src/css/components/IntentList.scss b/web/src/css/components/IntentList.scss new file mode 100644 index 0000000..fd5f29c --- /dev/null +++ b/web/src/css/components/IntentList.scss @@ -0,0 +1,52 @@ +$minHeight : calc(100vh - 80px); //80px is height of the title div container + + +.IntentList{ + width: 30%; + min-height:$minHeight; + float:left; + background-color: #eff0f0; +} + +.IntentList-title{ + font-size: 1.5em; + height: 70px; + padding: 20px; + text-align:left; + background-color: #eff0f0; +} + +.AddIntent{ + float: right; + margin: 12px; +} + +.LaunchRequestButton{ + text-align: left; + width: 100%; + height: 50px; + background-color: #d8d8d8 +} + +.LaunchRequestButton-selected{ + text-align: left; + width: 100%; + height: 50px; + background-color: #f5f5f5; +} + +.ContactButton{ + text-align: left; + color: #009b8a; + width: 100%; + height: 50px; + background-color: #d8d8d8 +} + +.ContactButton-selected{ + text-align: left; + color: #009b8a; + width: 100%; + height: 50px; + background-color: #f5f5f5; +} \ No newline at end of file diff --git a/web/src/css/components/LaunchRequest.scss b/web/src/css/components/LaunchRequest.scss new file mode 100644 index 0000000..35fc714 --- /dev/null +++ b/web/src/css/components/LaunchRequest.scss @@ -0,0 +1,11 @@ +.ExplanationText{ + float: left; + margin-top: 30px; + margin-left: 20px; + text-align: left; +} + +.InvocationInputBoxes{ + width: 60%; + margin-left: 20px; +} \ No newline at end of file From 1b04e72b985f8a7263f590a64ce7dd2351978c8a Mon Sep 17 00:00:00 2001 From: GotPPay Date: Mon, 15 Jan 2018 14:52:34 +0100 Subject: [PATCH 35/83] Revert changes --- web/src/App.js | 1 + 1 file changed, 1 insertion(+) diff --git a/web/src/App.js b/web/src/App.js index 571797a..b3bd1fe 100644 --- a/web/src/App.js +++ b/web/src/App.js @@ -126,6 +126,7 @@ class App extends Component { handleDeleteIntentClick(selectedIntent){ let id = -1; + //TODO : Change comparsion method ! Same object with different proeprty sorting will not be same string this.state.allIntents.map((intent,index)=>{ if ((id===-1) && (JSON.stringify(selectedIntent)===JSON.stringify(intent))) id = index; From e1f315cb812236d94a3eeea676df45971341ba86 Mon Sep 17 00:00:00 2001 From: GotPPay Date: Mon, 15 Jan 2018 16:06:37 +0100 Subject: [PATCH 36/83] backend api refactoring --- backend/controllers/index.js | 3 +-- backend/controllers/skill.js | 6 ++++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/backend/controllers/index.js b/backend/controllers/index.js index 9cf449c..93f7810 100644 --- a/backend/controllers/index.js +++ b/backend/controllers/index.js @@ -1,6 +1,5 @@ var express = require ('express'), router = express.Router (); -router.use ('/getSkill', require ('./skill')); -router.use ('/updateSkill', require ('./skill')); // this will be fixed in next code refactoring +router.use ('/skill', require ('./skill')); module.exports = router; diff --git a/backend/controllers/skill.js b/backend/controllers/skill.js index 4a5fc62..94cfecc 100644 --- a/backend/controllers/skill.js +++ b/backend/controllers/skill.js @@ -22,7 +22,7 @@ router.get ('/:id', async (req, res, next) => { } }); -router.post ('/:id', bodyParser.json (), async (req, res, next) => { +router.put ('/:id', bodyParser.json (), async (req, res, next) => { let id = req.params.id; let dataFromWeb = JSON.stringify (req.body); let skill = JSON.parse (dataFromWeb); @@ -33,7 +33,9 @@ router.post ('/:id', bodyParser.json (), async (req, res, next) => { console.log ('id = ' + id); - if (id !== '-1') { + //TODO : Fix inconsistency ! If skill is sent to amazon and accepted, but + //fails in database (ID doesn't exist) + if (id.length === constants.skillIDLength) { if (updateOnAmazon) { amazonHelper .updateSkill (skill) From 7e4b959ffa622caf82a0c87533616b16ce4cc7a4 Mon Sep 17 00:00:00 2001 From: GotPPay Date: Mon, 15 Jan 2018 16:09:09 +0100 Subject: [PATCH 37/83] backend api refactoring --- backend/config/constants.js | 2 +- backend/controllers/skill.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/config/constants.js b/backend/config/constants.js index 5895516..39913a3 100644 --- a/backend/config/constants.js +++ b/backend/config/constants.js @@ -15,7 +15,7 @@ constants.apiResultCodes = { ok:0, amazonError:1, databaseError:2, - + IDLengthError:3, } constants.skillIDLength = 24; diff --git a/backend/controllers/skill.js b/backend/controllers/skill.js index 94cfecc..b292f8d 100644 --- a/backend/controllers/skill.js +++ b/backend/controllers/skill.js @@ -86,7 +86,7 @@ router.put ('/:id', bodyParser.json (), async (req, res, next) => { }); } } else { - //no new skills for now + res.json ({result: constants.IDLengthError, message: ''}); } }); From 6e7bccea864e72a396f3cbb40a8bcfb7210f647b Mon Sep 17 00:00:00 2001 From: GotPPay Date: Mon, 15 Jan 2018 16:09:41 +0100 Subject: [PATCH 38/83] frontend api refactoring --- web/src/lib/api.js | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/web/src/lib/api.js b/web/src/lib/api.js index d55c12c..22d4bc1 100644 --- a/web/src/lib/api.js +++ b/web/src/lib/api.js @@ -1,24 +1,14 @@ import fetch from 'isomorphic-fetch'; import {BASE_URL} from '../config/config'; -export const getAllIntents = (id)=>{ - let url = `http://${BASE_URL}/intents/${id}` - return fetch(url, {method: 'GET'}); -} - export const getSkill = (id)=>{ - let url = `http://${BASE_URL}/getSkill/${id}` - return fetch(url, {method: 'GET'}); -} - -export const deleteSkill = (id)=>{ - let url = `http://${BASE_URL}/deleteSkill/${id}` + let url = `http://${BASE_URL}/skill/${id}` return fetch(url, {method: 'GET'}); } export const updateSkill = (skill)=>{ let id = (skill._id) ? skill._id : -1; - let url = `http://${BASE_URL}/updateSkill/${id}` + let url = `http://${BASE_URL}/skill/${id}` return fetch(url, { method: 'POST', headers: { From 6859f567a9aedee68b8cc092b068c30ac2448508 Mon Sep 17 00:00:00 2001 From: GotPPay Date: Mon, 15 Jan 2018 16:27:12 +0100 Subject: [PATCH 39/83] frontend api refactoring --- web/src/lib/api.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/lib/api.js b/web/src/lib/api.js index 22d4bc1..f2557e4 100644 --- a/web/src/lib/api.js +++ b/web/src/lib/api.js @@ -10,7 +10,7 @@ export const updateSkill = (skill)=>{ let id = (skill._id) ? skill._id : -1; let url = `http://${BASE_URL}/skill/${id}` return fetch(url, { - method: 'POST', + method: 'PUT', headers: { 'Accept': 'application/json', 'Content-Type': 'application/json' From cd938c39816b6aaba9e9a35a73ca3388d5ea6208 Mon Sep 17 00:00:00 2001 From: GotPPay Date: Mon, 15 Jan 2018 23:44:50 +0100 Subject: [PATCH 40/83] additional edit for api refactoring --- backend/middleware/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/middleware/index.js b/backend/middleware/index.js index 0b842fa..9543c0a 100644 --- a/backend/middleware/index.js +++ b/backend/middleware/index.js @@ -1,7 +1,7 @@ module.exports = function (req, res, next) { res.header ('Access-Control-Allow-Origin', '*'); res.header ('Access-Control-Allow-Headers', 'Origin, Content-Type'); - res.header ('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); + res.header ('Access-Control-Allow-Methods', 'GET, POST, PUT'); res.header ('Access-Control-Allow-Credentials', 'true'); next (); }; From a78189f1a16f11eece98d6b691ffac07ce20a9c3 Mon Sep 17 00:00:00 2001 From: GotPPay Date: Tue, 16 Jan 2018 00:08:38 +0100 Subject: [PATCH 41/83] removed doubled function ; return object instead of array with one element --- backend/components/alexa.js | 2 +- backend/helpers/database.js | 19 ++----------------- web/src/App.js | 9 ++++----- 3 files changed, 7 insertions(+), 23 deletions(-) diff --git a/backend/components/alexa.js b/backend/components/alexa.js index b06c44f..2fc124d 100644 --- a/backend/components/alexa.js +++ b/backend/components/alexa.js @@ -33,7 +33,7 @@ module.exports = { updateIntentsJSON: function () { databaseHelper - .loadSkill (config.SKILL_DB_ID) + .getSkill (config.SKILL_DB_ID) .then (skill => { skill.intents.map (intent => { alexaApp.intent ( diff --git a/backend/helpers/database.js b/backend/helpers/database.js index bd662a2..b5f3abd 100644 --- a/backend/helpers/database.js +++ b/backend/helpers/database.js @@ -52,7 +52,7 @@ module.exports = { }); }, - loadSkill: function (skillDbID) { + getSkill: function (skillDbID) { return new Promise ((resolve, reject) => { db .collection ('skill_list') @@ -92,20 +92,5 @@ module.exports = { } }); }); - }, - - getSkill: function (id) { - return new Promise ((resolve, reject) => { - db - .collection ('skill_list') - .find ({_id: ObjectID (id)}) - .toArray ((err, result) => { - if (err) { - reject (err); - }else{ - resolve (result); - } - }); - }); - }, + } }; diff --git a/web/src/App.js b/web/src/App.js index b3bd1fe..663d0ec 100644 --- a/web/src/App.js +++ b/web/src/App.js @@ -31,11 +31,10 @@ class App extends Component { }; getSkill(this.state._id).then(l=>l.json()).then(result=>{ - let jResult = result[0]; - if (jResult===undefined) return; - this.setState({ skillID:jResult.skillID,skillName:jResult.skillName, invocationName: jResult.invocationName, - invocationAnswer: jResult.invocationAnswer, - allIntents: jResult.intents, contactEmail: jResult.contactEmail}) + if (result===undefined) return; + this.setState({ skillID:result.skillID,skillName:result.skillName, invocationName: result.invocationName, + invocationAnswer: result.invocationAnswer, + allIntents: result.intents, contactEmail: result.contactEmail}) }) this.handleIntentClick = this.handleIntentClick.bind(this); From 1a5f4586a84e045e35f129d925ce8dd1d34a5b76 Mon Sep 17 00:00:00 2001 From: GotPPay Date: Tue, 16 Jan 2018 00:40:03 +0100 Subject: [PATCH 42/83] inconsistent state fix --- backend/config/constants.js | 8 ++- backend/controllers/skill.js | 105 ++++++++++++++++++++--------------- 2 files changed, 65 insertions(+), 48 deletions(-) diff --git a/backend/config/constants.js b/backend/config/constants.js index 39913a3..838b918 100644 --- a/backend/config/constants.js +++ b/backend/config/constants.js @@ -13,9 +13,11 @@ constants.amazonResultCodes = { constants.apiResultCodes = { genericError : -1, ok:0, - amazonError:1, - databaseError:2, - IDLengthError:3, + amazonError:1, //amazon api works, but error is some of the amazonResultCodes + amazonFail:2, //amazon api doesn't work + databaseError:3, + noSkill:4, + inconsistentState:5, } constants.skillIDLength = 24; diff --git a/backend/controllers/skill.js b/backend/controllers/skill.js index b292f8d..2af764a 100644 --- a/backend/controllers/skill.js +++ b/backend/controllers/skill.js @@ -33,60 +33,75 @@ router.put ('/:id', bodyParser.json (), async (req, res, next) => { console.log ('id = ' + id); - //TODO : Fix inconsistency ! If skill is sent to amazon and accepted, but - //fails in database (ID doesn't exist) - if (id.length === constants.skillIDLength) { - if (updateOnAmazon) { - amazonHelper - .updateSkill (skill) - .then (amazonResult => { - console.log ('Amazon : ' + amazonResult); - if ( - amazonResult === constants.amazonResultCodes.ok || - amazonResult === constants.amazonResultCodes.accepted - ) { - //Skill uploaded, it's ok to update databaseI - databaseHelper - .updateSkill (id, skill) - .then (result => { - res.json ({result: constants.apiResultCodes.ok, message: ''}); - alexa.updateIntentsJSON (); + if (updateOnAmazon) { + //First get current skill from DB + databaseHelper + .getSkill (id) + .then (skillInDB => { + //Now let's update skill in DB + databaseHelper + .updateSkill (id, skill) + .then (() => { + //Ok, done, now update skill on Amazon + amazonHelper + .updateSkill (skill) + .then (amazonResult => { + if ( + amazonResult === constants.amazonResultCodes.ok || + amazonResult === constants.amazonResultCodes.accepted + ) { + res.json ({result: constants.apiResultCodes.ok, message: ''}); + alexa.updateIntentsJSON (); + } else { + res.json ({ + result: constants.apiResultCodes.amazonError, + message: amazonResult, + }); + } }) .catch (e => { + res.json ({ + result: constants.apiResultCodes.amazonFail, + message: e, + }); + }); + }) + .catch (() => { + //Update in database didn't go well, revert changes + databaseHelper + .updateSkill (id, skillInDB) + .then (() => { res.json ({ result: constants.apiResultCodes.databaseError, message: '', }); + }) + .catch (() => { + //This should never happen, something is seriously wrong, like no database connection + res.json ({ + result: constants.apiResultCodes.inconsistentState, + message: '', + }); }); - } else { - res.json ({ - result: constants.apiResultCodes.amazonError, - message: amazonResult, - }); - } - }) - .catch (e => { - res.json ({ - result: constants.apiResultCodes.amazonError, - message: 'unknown', }); - }); - } else { - databaseHelper - .updateSkill (id, skill) - .then (result => { - res.json ({result: constants.apiResultCodes.ok, message: ''}); - alexa.updateIntentsJSON (); - }) - .catch (e => { - res.json ({ - result: constants.apiResultCodes.databaseError, - message: '', - }); - }); - } + }) + .catch (e => { + //I don't know why, but something went wrong, possibly ID of skill is wrong, doesn't exist in DB + res.json ({result: constants.apiResultCodes.noSkill, message: ''}); + }); } else { - res.json ({result: constants.IDLengthError, message: ''}); + databaseHelper + .updateSkill (id, skill) + .then (result => { + res.json ({result: constants.apiResultCodes.ok, message: ''}); + alexa.updateIntentsJSON (); + }) + .catch (e => { + res.json ({ + result: constants.apiResultCodes.databaseError, + message: '', + }); + }); } }); From e5783740cfdb0a9fd3334a25924ff60f031531ac Mon Sep 17 00:00:00 2001 From: GotPPay Date: Tue, 16 Jan 2018 01:44:33 +0100 Subject: [PATCH 43/83] fix token expired state --- backend/helpers/amazon.js | 25 ++++++++++++++----------- backend/helpers/database.js | 13 ++++++++----- 2 files changed, 22 insertions(+), 16 deletions(-) diff --git a/backend/helpers/amazon.js b/backend/helpers/amazon.js index 3a66cbf..1fd9410 100644 --- a/backend/helpers/amazon.js +++ b/backend/helpers/amazon.js @@ -35,17 +35,20 @@ var refreshTokens = function () { }; request (options, function (error, response, body) { - if (error) reject (error); - parsedResponse = JSON.parse (body); - if (parsedResponse.refresh_token) - return databaseHelper.updateTokens ( - parsedResponse.refresh_token, - parsedResponse.access_token, - parsedResponse.expires_in - ); - console.log ('Token refresh failed'); - console.log (body); - reject (body); + if (error) { + reject (error); + }else{ + parsedResponse = JSON.parse (body); + if (parsedResponse.refresh_token){ + databaseHelper.updateTokens(parsedResponse.refresh_token, parsedResponse.access_token, parsedResponse.expires_in).then(()=>{ + resolve(); + }).catch(e=>{ + reject(e); + }); + }else{ + reject (body); + } + } }); }); }; diff --git a/backend/helpers/database.js b/backend/helpers/database.js index b5f3abd..6934339 100644 --- a/backend/helpers/database.js +++ b/backend/helpers/database.js @@ -43,11 +43,14 @@ module.exports = { db .collection ('token_list') .update ({id: 1}, newTokenDocument, {upsert: true}, (err, result) => { - if (err) reject (err); - config.REFRESH_TOKEN = refresh_token; - config.TOKEN = access_token; - config.TOKEN_EXPIRES_IN = newTokenDocument.expires_in; - resolve (); + if (err) { + reject (err) + }else{ + config.REFRESH_TOKEN = refresh_token; + config.TOKEN = access_token; + config.TOKEN_EXPIRES_IN = newTokenDocument.expires_in; + resolve (); + } }); }); }, From 389a71b05a6426dece69b4d41cb6cf5964e856ac Mon Sep 17 00:00:00 2001 From: GotPPay Date: Tue, 16 Jan 2018 13:38:05 +0100 Subject: [PATCH 44/83] UI fix --- web/src/components/IntentDetails.js | 8 ++++---- web/src/components/IntentList.js | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/web/src/components/IntentDetails.js b/web/src/components/IntentDetails.js index 5af5471..e5c6066 100644 --- a/web/src/components/IntentDetails.js +++ b/web/src/components/IntentDetails.js @@ -28,13 +28,13 @@ class IntentDetails extends Component { id="intent name" lineDirection="center" placeholder="Intent name" - label="Intent name" + label="Question name" className="md-cell md-cell--bottom IntentDetailsInputBoxes" onChange={this.handleIntentNameEdit} maxLength={INTENT_NAME_MAX_LENGTH} value={this.state.intent.intentName} />
-
Questions
+
Question variants
{ this.state.intent.questions.map((question, index)=>{ return ( @@ -68,8 +68,8 @@ class IntentDetails extends Component { }



- - + +
diff --git a/web/src/components/IntentList.js b/web/src/components/IntentList.js index 96a86d1..00596fc 100644 --- a/web/src/components/IntentList.js +++ b/web/src/components/IntentList.js @@ -30,7 +30,7 @@ class IntentList extends Component {
-

Intents

+

Questions

{ this.state.intents.map((intent,index)=>{ @@ -45,7 +45,7 @@ class IntentList extends Component {

+ disabled={this.props.waiting}>Add question
); } From 5db270d957e10a79567e96d85a935acf49008d89 Mon Sep 17 00:00:00 2001 From: GotPPay Date: Tue, 16 Jan 2018 13:48:39 +0100 Subject: [PATCH 45/83] fix missing css on build --- web/package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/web/package.json b/web/package.json index 217a9a8..e4474aa 100644 --- a/web/package.json +++ b/web/package.json @@ -17,7 +17,8 @@ "watch-css": "nodemon -e scss -x \"npm run watch-css-mine\"", "start-js": "react-scripts start", "start": "npm-run-all -p watch-css start-js", - "build": "react-scripts build", + "react-build" : "react-scripts build", + "build": "npm-run-all -p build-css react-build", "test": "react-scripts test --env=jsdom", "eject": "react-scripts eject" }, From cd041c0131e1ad616e56710743dd791a7ee20601 Mon Sep 17 00:00:00 2001 From: GotPPay Date: Tue, 16 Jan 2018 16:56:56 +0100 Subject: [PATCH 46/83] code improvements --- backend/config/config.js | 2 +- backend/config/constants.js | 34 ++++++------ backend/controllers/skill.js | 100 +++++++++++++++-------------------- backend/server.js | 2 +- 4 files changed, 65 insertions(+), 73 deletions(-) diff --git a/backend/config/config.js b/backend/config/config.js index 3edee7d..67573de 100644 --- a/backend/config/config.js +++ b/backend/config/config.js @@ -1,6 +1,6 @@ var config = {}; -config.dbURL = 'mongodb://localhost:27017/tellall'; +config.DB_URL = 'mongodb://localhost:27017/tellall'; config.PORT = 5000; config.TOKEN = 'Atza|IwEBIBe6gDqrrowEEav6N-_6s4NztYeP3oG8PGWmu8ZiZw6lbOh3wNla3TK6pY-VEpT1d8an-dVf_n3kXJzVFsNo_4xBfZyFHGoCTDTFjs3yBRul4PVdBOhwwiH3-sgRLcUofZbe2oE06GmTcbfYtaStfXpQI5dfpldfnsJg_CvhSA6AHb_snJT3F6lyXzbV076d_3cYUMJxFldJGnYcviNHHxjjmuQTD06hhGzCbAxxe9eBmkuopRsNfyedLT2UlKP_ublah9CUGA3AdIX_3Iuke82jMwGnNl9gv7pbaDNEjAbj7IQSl3B08uuREtJq-oTBOjALNXRvFxTJmQjZwXNf9eHC7fSHJDdEPdZQU0AcffRQObAyAkUuL6Jv39OHzhb3Q64-zzoyODqnJyLP5SQZ2JVF53Kc_cTBqjIc9pXljqe7yEVk6JDs7q1zKbBibx_AQm57TO79IzWyLBzBMlYL5HdTsqEfRzLeDw2tws-hGMgkx2HWfdbYnmf5Qb4SyIhzvmmdfPLg3MVKTxjIBu1rx0xf3n0PLZP1EO6jsJPoMRPg77Gm4oit5Zp6s37ek3A3Vxh-ntoASpkrkxGTG9kVtRNt'; diff --git a/backend/config/constants.js b/backend/config/constants.js index 838b918..0fe470f 100644 --- a/backend/config/constants.js +++ b/backend/config/constants.js @@ -1,26 +1,30 @@ const constants = {}; constants.amazonResultCodes = { - ok:200, - accepted:202, - badRequest:400, - unauthorized:401, - notFound:404, - conflict:409, - payloadTooLarge:413 + OK:200, + ACCEPTED:202, + BAD_REQUEST:400, + UNAUTHORIZED:401, + NOT_FOUND:404, + CONFLICT:409, + PAYLOAD_TOO_LARGE:413 } constants.apiResultCodes = { - genericError : -1, - ok:0, - amazonError:1, //amazon api works, but error is some of the amazonResultCodes - amazonFail:2, //amazon api doesn't work - databaseError:3, - noSkill:4, - inconsistentState:5, + GENERIC_ERROR : -1, + OK:0, + AMAZON_ERROR:1, //amazon api works, but error is some of the amazonResultCodes + AMAZON_FAIL:2, //amazon api doesn't work + DATABASE_ERROR:3, + NO_SKILL:4, + INCONSISTEN_STATE:5, } -constants.skillIDLength = 24; +constants.HTTPResultCodes = { + INTERNAL_SERVER_ERROR : 500, +} + +constants.SKILL_ID_LENGTH = 24; diff --git a/backend/controllers/skill.js b/backend/controllers/skill.js index 2af764a..1d8cd44 100644 --- a/backend/controllers/skill.js +++ b/backend/controllers/skill.js @@ -8,7 +8,7 @@ var alexa = require ('../components/alexa'); router.get ('/:id', async (req, res, next) => { const id = req.params.id; - if (id.length !== constants.skillIDLength) { + if (id.length !== constants.SKILL_ID_LENGTH) { res.json ([]); } else { databaseHelper @@ -31,78 +31,66 @@ router.put ('/:id', bodyParser.json (), async (req, res, next) => { delete skill.updateOnAmazon; delete skill._id; - console.log ('id = ' + id); - - if (updateOnAmazon) { - //First get current skill from DB - databaseHelper - .getSkill (id) - .then (skillInDB => { - //Now let's update skill in DB - databaseHelper - .updateSkill (id, skill) - .then (() => { - //Ok, done, now update skill on Amazon + //First get current skill from DB + databaseHelper + .getSkill (id) + .then (currentSkillState => { + //Now let's update skill in DB + databaseHelper + .updateSkill (id, skill) + .then (() => { + //Ok, done, now update skill on Amazon (if needed) + if (updateOnAmazon) { amazonHelper .updateSkill (skill) .then (amazonResult => { if ( - amazonResult === constants.amazonResultCodes.ok || - amazonResult === constants.amazonResultCodes.accepted + amazonResult === constants.amazonResultCodes.OK || + amazonResult === constants.amazonResultCodes.ACCEPTED ) { - res.json ({result: constants.apiResultCodes.ok, message: ''}); + res.json ({result: constants.apiResultCodes.OK, message: ''}); alexa.updateIntentsJSON (); } else { - res.json ({ - result: constants.apiResultCodes.amazonError, + res.status(constants.HTTPResultCodes.INTERNAL_SERVER_ERROR).json ({ + result: constants.apiResultCodes.AMAZON_ERROR, message: amazonResult, }); } }) .catch (e => { - res.json ({ - result: constants.apiResultCodes.amazonFail, + res.status(constants.HTTPResultCodes.INTERNAL_SERVER_ERROR).json ({ + result: constants.apiResultCodes.AMAZON_FAIL, message: e, }); }); - }) - .catch (() => { - //Update in database didn't go well, revert changes - databaseHelper - .updateSkill (id, skillInDB) - .then (() => { - res.json ({ - result: constants.apiResultCodes.databaseError, - message: '', - }); - }) - .catch (() => { - //This should never happen, something is seriously wrong, like no database connection - res.json ({ - result: constants.apiResultCodes.inconsistentState, - message: '', - }); + }else{ + res.json ({result: constants.apiResultCodes.OK, message: ''}); + alexa.updateIntentsJSON (); + } + }) + .catch (() => { + //Update in database didn't go well, revert changes + databaseHelper + .updateSkill (id, currentSkillState) + .then (() => { + res.status(constants.HTTPResultCodes.INTERNAL_SERVER_ERROR).json ({ + result: constants.apiResultCodes.DATABASE_ERROR, + message: '', }); - }); - }) - .catch (e => { - //I don't know why, but something went wrong, possibly ID of skill is wrong, doesn't exist in DB - res.json ({result: constants.apiResultCodes.noSkill, message: ''}); - }); - } else { - databaseHelper - .updateSkill (id, skill) - .then (result => { - res.json ({result: constants.apiResultCodes.ok, message: ''}); - alexa.updateIntentsJSON (); - }) - .catch (e => { - res.json ({ - result: constants.apiResultCodes.databaseError, - message: '', + }) + .catch (() => { + //This should never happen, something is seriously wrong, like no database connection + res.status(constants.HTTPResultCodes.INTERNAL_SERVER_ERROR).json ({ + result: constants.apiResultCodes.INCONSISTEN_STATE, + message: '', + }); + }); }); - }); - } + }) + .catch (e => { + //I don't know why, but something went wrong, possibly ID of skill is wrong, doesn't exist in DB + res.status(constants.HTTPResultCodes.INTERNAL_SERVER_ERROR).json ({result: constants.apiResultCodes.NO_SKILL, message: ''}); + }); }); module.exports = router; diff --git a/backend/server.js b/backend/server.js index cbed3c5..859389c 100644 --- a/backend/server.js +++ b/backend/server.js @@ -20,7 +20,7 @@ app.set ('view engine', 'ejs'); app.use (require ('./middleware')); //common middleware for all requests app.use (require ('./controllers')); //all routes -MongoClient.connect (config.dbURL) +MongoClient.connect (config.DB_URL) .then (database => { databaseHelper.initModule (database); From 2f59e12aa75fcbd6a1da1125549fb15af224bb0b Mon Sep 17 00:00:00 2001 From: GotPPay Date: Thu, 18 Jan 2018 19:50:13 +0100 Subject: [PATCH 47/83] test new library --- backend/controllers/index.js | 1 + backend/controllers/saburlyEntryPoint.js | 40 ++++++++++++++++++++++++ backend/package.json | 1 + backend/server.js | 6 ++-- 4 files changed, 45 insertions(+), 3 deletions(-) create mode 100644 backend/controllers/saburlyEntryPoint.js diff --git a/backend/controllers/index.js b/backend/controllers/index.js index 93f7810..bdcb063 100644 --- a/backend/controllers/index.js +++ b/backend/controllers/index.js @@ -1,5 +1,6 @@ var express = require ('express'), router = express.Router (); router.use ('/skill', require ('./skill')); +router.use ('/saburly', require('./saburlyEntryPoint')); module.exports = router; diff --git a/backend/controllers/saburlyEntryPoint.js b/backend/controllers/saburlyEntryPoint.js new file mode 100644 index 0000000..8b3f90e --- /dev/null +++ b/backend/controllers/saburlyEntryPoint.js @@ -0,0 +1,40 @@ +var express = require ('express'), router = express.Router (); +const config = require('../config/config'); +var bodyParser = require ('body-parser'); +var Alexa = require('alexa-sdk'); + + +router.get('/', async (req, res) => { + console.log("GET request on /saburly"); + // Build the context manually, because Amazon Lambda is missing + var context = { + succeed: function (result) { + console.log(result); + res.json(result); + }, + fail:function (error) { + console.log(error); + } + }; + + const handlers = { + 'LaunchRequest': function () { + console.log("Launch request"); + this.emit(':tell', 'Welcome to Saburly'); + this.emit('HelloWorldIntent'); + }, + + 'HelloWorldIntent': function () { + console.log("Hello world intent"); + this.emit(':tell', 'Hello World!'); + } + }; + + // Delegate the request to the Alexa SDK and the declared intent-handlers + var alexa = Alexa.handler(req.body, context); + alexa.appId = config.SKILL_ID; + alexa.registerHandlers(handlers); + alexa.execute(); +}); + +module.exports = router; \ No newline at end of file diff --git a/backend/package.json b/backend/package.json index 74d58ac..421c735 100644 --- a/backend/package.json +++ b/backend/package.json @@ -5,6 +5,7 @@ "main": "test.js", "dependencies": { "alexa-app": "4.2.0", + "alexa-sdk": "^1.0.25", "body-parser": "^1.13.1", "ejs": "^2.5.7", "express": "^4.13.0", diff --git a/backend/server.js b/backend/server.js index 859389c..32e38e1 100644 --- a/backend/server.js +++ b/backend/server.js @@ -4,7 +4,7 @@ const config = require ('./config/config'); const constants = require ('./config/constants'); require ('isomorphic-fetch'); var express = require ('express'); -var alexa = require('./components/alexa'); +//var alexa = require('./components/alexa'); var MongoClient = require ('mongodb').MongoClient; @@ -14,7 +14,7 @@ const router = express.Router (); var app = express (); // ALWAYS setup the alexa app and attach it to express before anything else. -alexa.init (app); +//alexa.init (app); app.set ('view engine', 'ejs'); app.use (require ('./middleware')); //common middleware for all requests @@ -26,7 +26,7 @@ MongoClient.connect (config.DB_URL) app.listen (config.PORT, () => { console.log ('Express server running on port ' + config.PORT); - alexa.updateIntentsJSON (); + //alexa.updateIntentsJSON (); databaseHelper.loadTokens (); }); }) From 68287d49fff8e474f7cb869eafcedab3fb4b5069 Mon Sep 17 00:00:00 2001 From: GotPPay Date: Thu, 18 Jan 2018 21:33:44 +0100 Subject: [PATCH 48/83] complete switch to new alexa package --- backend/components/alexa.js | 204 ----------------------- backend/controllers/saburlyEntryPoint.js | 39 +---- backend/controllers/skill.js | 6 +- backend/helpers/amazon.js | 2 + backend/models/alexa.js | 50 ++++++ backend/package.json | 1 - backend/server.js | 13 +- 7 files changed, 62 insertions(+), 253 deletions(-) delete mode 100644 backend/components/alexa.js create mode 100644 backend/models/alexa.js diff --git a/backend/components/alexa.js b/backend/components/alexa.js deleted file mode 100644 index 2fc124d..0000000 --- a/backend/components/alexa.js +++ /dev/null @@ -1,204 +0,0 @@ -var alexa = require ('alexa-app'); -const config = require ('../config/config'); -var databaseHelper = require ('../helpers/database'); - -//User data for sending message, this is skill-related and will be in some skill container -var Name = null; -var Email = null; -var Message = null; -var State = 0; // states should be defined in seperate file. (Not sending message, Waiting for name, Waiting for email, Waiting for message) -//For now (this is not long term solution) -// 0 : Not sending Message -// 1 : Waiting for name -// 2 : Waiting for email -// 3 : Waiting for message - -var alexaApp = new alexa.app ('saburly'); // this means we still work with one skill - -module.exports = { - init: function (express) { - alexaApp.express ({ - expressApp: express, - - // verifies requests come from amazon alexa. Must be enabled for production. - // You can disable this if you're running a dev environment and want to POST - // things to test behavior. enabled by default. - checkCert: false, - - // sets up a GET route when set to true. This is handy for testing in - // development, but not recommended for production. disabled by default - debug: true, - }); - }, - - updateIntentsJSON: function () { - databaseHelper - .getSkill (config.SKILL_DB_ID) - .then (skill => { - skill.intents.map (intent => { - alexaApp.intent ( - intent.intentName, - { - slots: [], - utterances: intent.questions, - }, - function (request, response) { - return response.say (intent.answer).shouldEndSession (false); - } - ); - }); - - alexaApp.launch ((request, response) => { - return response.say (skill.invocationAnswer).shouldEndSession (false); - }); - - alexaApp.intent ( - 'EmailIntentLaunch', - { - slots: [], - utterances: [ - 'I want to send a message', - 'I would like to send a message', - 'I would like to leave a message', - 'Leave a message', - ], - }, - function (request, response) { - Name = null; - Email = null; - Message = null; - State = 1; - - return response - .say ('Ok. What is your name') - .shouldEndSession (false); - } - ); - - //TODO : Watch out for this intent. It will make trouble with other regular intents - //if other intents have utterance with just one slot like {Data} - //It should be taken care somwhere before this, to check if Email Intent is invoked - //This is problem only if we introduce slot options for regular intents for users - alexaApp.intent ( - 'EmailIntent', - { - slots: { - Name: 'AMAZON.US_FIRST_NAME', - Email: 'AMAZON.LITERAL', - Message: 'AMAZON.LITERAL', - Data: 'AMAZON.LITERAL', - }, - utterances: [ - 'My name is {-|Name}', - 'I am {-|Name}', - '{dawdw at dwd wdw|Data}', - 'My email is {wadwwdw at wadwwd wdw|Email}', - 'Send replay to {fkofkeofe at dppfam wd|Email}', - 'My message is {Quick brown fox jumps over lazy dog|Message}', - ], - }, - function (request, response) { - let Data = undefined; - try { - if (!Name) Name = request.slot ('Name'); - } catch (e) { - console.log ('Error. No name slot '); - Name = undefined; - } - try { - if (!Email) Email = request.slot ('Email'); - } catch (e) { - console.log ('Error. No Email slot'); - Email = undefined; - } - try { - if (!Message) Message = request.slot ('Message'); - } catch (e) { - console.log ('Error. No Message slot'); - Message = undefined; - } - try { - Data = request.slot ('Data'); - } catch (e) { - console.log ('Error. No Data slot'); - Data = undefined; - } - console.log ('State : ' + State); - - //TODO : Responses could be configurable for each skill ? - - if (State === 1) { - //Was waiting for name, so if Name is null, name is probably in Data - if ((!Name && Data) || Name) { - //got the name, let's continue for the email - if (!Name) Name = Data; - State = 2; - return response - .say ('Ok ' + Name + '. What is your email ?') - .shouldEndSession (false); - } else { - //Something is wrong, ask for name again - return response - .say ( - 'Sorry, I didnt understand your name. Can you say it again ?' - ) - .shouldEndSession (false); - } - } else if (State === 2) { - //was waiting for email, so if Email is null, email is probably in Data - if ((!Email && Data) || Email) { - //Got the email, first verify email and than continue to message - if (!Email) Email = Data; - //TODO : verify email - State = 3; - return response - .say ('Great. Whats the message ?') - .shouldEndSession (false); - } else { - //Something is wrong, ask for the email again - return response - .say ( - 'Sorry, I didnt understan you email. Can you say it again ?' - ) - .shouldEndSession (false); - } - } else if (State === 3) { - //Was waiting for message, so if Message is null, message is probably in Data - if ((!Message && Data) || Message) { - //Ok, we got all informations. Exit email intent - if (!Message) Message = Data; - State = 0; - //TODO : Send email - console.log ( - 'Name : ' + - Name + - ' | Email : ' + - Email + - ' | Message : ' + - Message - ); - return response.say ( - 'Message sent. Someone will contact you ASAP' - ); - } else { - //Something is wrong, ask for the message again - return response - .say ( - 'Sorry, I didnt understand your message. Can you say it again ?' - ) - .shouldEndSession (false); - } - } else { - console.log ('State strange ! ' + State); - } - } - ); - }) - .catch (err => { - console.log (err); - alexaApp.launch ((request, response) => { - return response.say ('Sorry, there was no skill with that name'); - }); - }); - }, -}; diff --git a/backend/controllers/saburlyEntryPoint.js b/backend/controllers/saburlyEntryPoint.js index 8b3f90e..e698d84 100644 --- a/backend/controllers/saburlyEntryPoint.js +++ b/backend/controllers/saburlyEntryPoint.js @@ -1,40 +1,9 @@ var express = require ('express'), router = express.Router (); -const config = require('../config/config'); var bodyParser = require ('body-parser'); -var Alexa = require('alexa-sdk'); +var alexa = require ('../models/alexa'); - -router.get('/', async (req, res) => { - console.log("GET request on /saburly"); - // Build the context manually, because Amazon Lambda is missing - var context = { - succeed: function (result) { - console.log(result); - res.json(result); - }, - fail:function (error) { - console.log(error); - } - }; - - const handlers = { - 'LaunchRequest': function () { - console.log("Launch request"); - this.emit(':tell', 'Welcome to Saburly'); - this.emit('HelloWorldIntent'); - }, - - 'HelloWorldIntent': function () { - console.log("Hello world intent"); - this.emit(':tell', 'Hello World!'); - } - }; - - // Delegate the request to the Alexa SDK and the declared intent-handlers - var alexa = Alexa.handler(req.body, context); - alexa.appId = config.SKILL_ID; - alexa.registerHandlers(handlers); - alexa.execute(); +router.post ('/', bodyParser.json (), async (req, res) => { + alexa.run (req, res); }); -module.exports = router; \ No newline at end of file +module.exports = router; diff --git a/backend/controllers/skill.js b/backend/controllers/skill.js index 1d8cd44..6cc36e7 100644 --- a/backend/controllers/skill.js +++ b/backend/controllers/skill.js @@ -3,7 +3,7 @@ const constants = require ('../config/constants'); var databaseHelper = require ('../helpers/database'); var amazonHelper = require ('../helpers/amazon'); var bodyParser = require ('body-parser'); -var alexa = require ('../components/alexa'); +var alexa = require ('../models/alexa'); router.get ('/:id', async (req, res, next) => { const id = req.params.id; @@ -49,7 +49,7 @@ router.put ('/:id', bodyParser.json (), async (req, res, next) => { amazonResult === constants.amazonResultCodes.ACCEPTED ) { res.json ({result: constants.apiResultCodes.OK, message: ''}); - alexa.updateIntentsJSON (); + alexa.updateModel (); } else { res.status(constants.HTTPResultCodes.INTERNAL_SERVER_ERROR).json ({ result: constants.apiResultCodes.AMAZON_ERROR, @@ -65,7 +65,7 @@ router.put ('/:id', bodyParser.json (), async (req, res, next) => { }); }else{ res.json ({result: constants.apiResultCodes.OK, message: ''}); - alexa.updateIntentsJSON (); + alexa.updateModel (); } }) .catch (() => { diff --git a/backend/helpers/amazon.js b/backend/helpers/amazon.js index 1fd9410..d6c554b 100644 --- a/backend/helpers/amazon.js +++ b/backend/helpers/amazon.js @@ -82,6 +82,7 @@ var generateInteractionModel = function (skill) { }); //Special Email Intents : + /* allIntents.push ({ name: 'EmailIntentLaunch', slots: [], @@ -123,6 +124,7 @@ var generateInteractionModel = function (skill) { 'My message is {The quick brown fox jumps over the lazy dog.The quick brown fox jumps over the lazy dog.The quick brown fox jumps over the lazy dog.|Message}', ], }); + */ result.interactionModel = {}; diff --git a/backend/models/alexa.js b/backend/models/alexa.js new file mode 100644 index 0000000..a4b0471 --- /dev/null +++ b/backend/models/alexa.js @@ -0,0 +1,50 @@ +var Alexa = require ('alexa-sdk'); +const config = require ('../config/config'); +var databaseHelper = require ('../helpers/database'); + +// Build the context manually, because Amazon Lambda is missing +var context = { + succeed: function (result) { + console.log (result); + res.json (result); + }, + fail: function (error) { + console.log (error); + //We could send error json from here + }, +}; + +var handlers = {}; + +module.exports = { + run: function (req, res) { + var alexa = Alexa.handler (req.body, context); + alexa.appId = config.SKILL_ID; + alexa.registerHandlers (handlers); + alexa.execute (); + }, + updateModel: function () { + //Get info from database, and store it for faster response on intent + databaseHelper + .getSkill (config.SKILL_DB_ID) + .then (activeSkill => { + handlers = {}; + handlers = { + LaunchRequest: function () { + this.response.speak (activeSkill.invocationAnswer); + this.emit (':responseReady'); + }, + }; + activeSkill.intents.map (intent => { + handlers[intent.intentName] = function () { + this.response.speak (intent.answer); + this.emit (':responseReady'); + }; + }); + }) + .catch (e => { + //Something is wrong, skill is not ready, use catch-all intent to inform user + console.log ('Error. Skill doesnt exist'); + }); + }, +}; diff --git a/backend/package.json b/backend/package.json index 421c735..b0c0319 100644 --- a/backend/package.json +++ b/backend/package.json @@ -4,7 +4,6 @@ "description": "", "main": "test.js", "dependencies": { - "alexa-app": "4.2.0", "alexa-sdk": "^1.0.25", "body-parser": "^1.13.1", "ejs": "^2.5.7", diff --git a/backend/server.js b/backend/server.js index 32e38e1..f39f9b9 100644 --- a/backend/server.js +++ b/backend/server.js @@ -1,11 +1,7 @@ -var amazonHelper = require ('./helpers/amazon'); var databaseHelper = require ('./helpers/database'); const config = require ('./config/config'); -const constants = require ('./config/constants'); -require ('isomorphic-fetch'); var express = require ('express'); -//var alexa = require('./components/alexa'); - +var alexa = require ('./models/alexa'); var MongoClient = require ('mongodb').MongoClient; var ObjectID = require ('mongodb').ObjectID; @@ -13,10 +9,7 @@ var ObjectID = require ('mongodb').ObjectID; const router = express.Router (); var app = express (); -// ALWAYS setup the alexa app and attach it to express before anything else. -//alexa.init (app); - -app.set ('view engine', 'ejs'); +app.set ('view engine', 'ejs'); // Should be removed app.use (require ('./middleware')); //common middleware for all requests app.use (require ('./controllers')); //all routes @@ -26,7 +19,7 @@ MongoClient.connect (config.DB_URL) app.listen (config.PORT, () => { console.log ('Express server running on port ' + config.PORT); - //alexa.updateIntentsJSON (); + alexa.updateModel (); databaseHelper.loadTokens (); }); }) From 48badce0f085e59799315c2944fe1d70a4add687 Mon Sep 17 00:00:00 2001 From: GotPPay Date: Thu, 18 Jan 2018 21:43:25 +0100 Subject: [PATCH 49/83] fix 'res not defined' --- backend/models/alexa.js | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/backend/models/alexa.js b/backend/models/alexa.js index a4b0471..ebfce94 100644 --- a/backend/models/alexa.js +++ b/backend/models/alexa.js @@ -2,22 +2,22 @@ var Alexa = require ('alexa-sdk'); const config = require ('../config/config'); var databaseHelper = require ('../helpers/database'); -// Build the context manually, because Amazon Lambda is missing -var context = { - succeed: function (result) { - console.log (result); - res.json (result); - }, - fail: function (error) { - console.log (error); - //We could send error json from here - }, -}; - var handlers = {}; module.exports = { run: function (req, res) { + // Build the context manually, because Amazon Lambda is missing + var context = { + succeed: function (result) { + console.log (result); + res.json (result); + }, + fail: function (error) { + console.log (error); + //We could send error json from here + }, + }; + var alexa = Alexa.handler (req.body, context); alexa.appId = config.SKILL_ID; alexa.registerHandlers (handlers); From 6bfd4adcaf84e3be1cb695c7a06d48069e4be5a3 Mon Sep 17 00:00:00 2001 From: GotPPay Date: Thu, 18 Jan 2018 21:50:26 +0100 Subject: [PATCH 50/83] keep session open --- backend/models/alexa.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/backend/models/alexa.js b/backend/models/alexa.js index ebfce94..20a5e34 100644 --- a/backend/models/alexa.js +++ b/backend/models/alexa.js @@ -31,13 +31,15 @@ module.exports = { handlers = {}; handlers = { LaunchRequest: function () { - this.response.speak (activeSkill.invocationAnswer); + this.response.listen (activeSkill.invocationAnswer);// Using listen so session doesn't end + //TODO : Maybe to ask user does he want to hear possible intents + //For this functionality, we need explanation text for each intent (Question) this.emit (':responseReady'); }, }; activeSkill.intents.map (intent => { handlers[intent.intentName] = function () { - this.response.speak (intent.answer); + this.response.listen (intent.answer); //Using listen so session doesn't end this.emit (':responseReady'); }; }); From 9e4b06bd4c540554984690199420e78ce3c580f7 Mon Sep 17 00:00:00 2001 From: GotPPay Date: Thu, 18 Jan 2018 21:56:56 +0100 Subject: [PATCH 51/83] keep session open - fix --- backend/models/alexa.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/backend/models/alexa.js b/backend/models/alexa.js index 20a5e34..ad58067 100644 --- a/backend/models/alexa.js +++ b/backend/models/alexa.js @@ -24,6 +24,8 @@ module.exports = { alexa.execute (); }, updateModel: function () { + //TODO : Alexa-sdk has handlers like SessionEndedRequest, Unhandled ,... that can be used + //Get info from database, and store it for faster response on intent databaseHelper .getSkill (config.SKILL_DB_ID) @@ -31,7 +33,9 @@ module.exports = { handlers = {}; handlers = { LaunchRequest: function () { - this.response.listen (activeSkill.invocationAnswer);// Using listen so session doesn't end + this.response + .speak (activeSkill.invocationAnswer) + .listen (' Would you like to continue ?'); //TODO : Maybe to ask user does he want to hear possible intents //For this functionality, we need explanation text for each intent (Question) this.emit (':responseReady'); @@ -39,7 +43,9 @@ module.exports = { }; activeSkill.intents.map (intent => { handlers[intent.intentName] = function () { - this.response.listen (intent.answer); //Using listen so session doesn't end + this.response + .speak (intent.answer) + .listen ('Would you like to continue ?'); this.emit (':responseReady'); }; }); From a570640fe14aebee78860973d43d5ea9450b62bf Mon Sep 17 00:00:00 2001 From: GotPPay Date: Thu, 18 Jan 2018 22:07:23 +0100 Subject: [PATCH 52/83] hande unknown questions --- backend/config/constants.js | 5 +++++ backend/models/alexa.js | 25 +++++++++++++++++++++---- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/backend/config/constants.js b/backend/config/constants.js index 0fe470f..6674956 100644 --- a/backend/config/constants.js +++ b/backend/config/constants.js @@ -26,6 +26,11 @@ constants.HTTPResultCodes = { constants.SKILL_ID_LENGTH = 24; +constants.voiceResponseStrings = { + QUESTION_NOT_FOUND : 'Sorry, I didnt understan', + GENERIC_CONTINUE : 'Would you like to continue' +} + module.exports = constants; \ No newline at end of file diff --git a/backend/models/alexa.js b/backend/models/alexa.js index ad58067..d95c855 100644 --- a/backend/models/alexa.js +++ b/backend/models/alexa.js @@ -1,6 +1,7 @@ var Alexa = require ('alexa-sdk'); const config = require ('../config/config'); var databaseHelper = require ('../helpers/database'); +const constants = require ('../config/constants'); var handlers = {}; @@ -24,31 +25,47 @@ module.exports = { alexa.execute (); }, updateModel: function () { - //TODO : Alexa-sdk has handlers like SessionEndedRequest, Unhandled ,... that can be used - //Get info from database, and store it for faster response on intent databaseHelper .getSkill (config.SKILL_DB_ID) .then (activeSkill => { handlers = {}; + + //Handler for launch request handlers = { LaunchRequest: function () { this.response .speak (activeSkill.invocationAnswer) - .listen (' Would you like to continue ?'); + .listen (constants.voiceResponseStrings.GENERIC_CONTINUE); //Phrase from listen doesn't work !!! //TODO : Maybe to ask user does he want to hear possible intents //For this functionality, we need explanation text for each intent (Question) this.emit (':responseReady'); }, }; + + //Handlers for user defined questions activeSkill.intents.map (intent => { handlers[intent.intentName] = function () { this.response .speak (intent.answer) - .listen ('Would you like to continue ?'); + .listen (constants.voiceResponseStrings.GENERIC_CONTINUE); //Phrase from listen doesn't work !!! this.emit (':responseReady'); }; }); + + //Default handlers for unknown questions and session close + + handlers.Unhandled = function () { + this.response + .speak (constants.voiceResponseStrings.QUESTION_NOT_FOUND) + .listen (constants.voiceResponseStrings.GENERIC_CONTINUE); + } + + handlers.SessionEndedRequest = function(){ + //We don't care for now + } + + }) .catch (e => { //Something is wrong, skill is not ready, use catch-all intent to inform user From b07a9e21b363aa4305b2d30afd148c6db6f8ea0d Mon Sep 17 00:00:00 2001 From: GotPPay Date: Fri, 19 Jan 2018 07:43:10 +0100 Subject: [PATCH 53/83] Testing dialog handler --- backend/helpers/amazon.js | 221 ++++++++++++++++++++++++++++---------- backend/models/alexa.js | 15 ++- 2 files changed, 174 insertions(+), 62 deletions(-) diff --git a/backend/helpers/amazon.js b/backend/helpers/amazon.js index d6c554b..6d1b553 100644 --- a/backend/helpers/amazon.js +++ b/backend/helpers/amazon.js @@ -37,15 +37,22 @@ var refreshTokens = function () { request (options, function (error, response, body) { if (error) { reject (error); - }else{ + } else { parsedResponse = JSON.parse (body); - if (parsedResponse.refresh_token){ - databaseHelper.updateTokens(parsedResponse.refresh_token, parsedResponse.access_token, parsedResponse.expires_in).then(()=>{ - resolve(); - }).catch(e=>{ - reject(e); - }); - }else{ + if (parsedResponse.refresh_token) { + databaseHelper + .updateTokens ( + parsedResponse.refresh_token, + parsedResponse.access_token, + parsedResponse.expires_in + ) + .then (() => { + resolve (); + }) + .catch (e => { + reject (e); + }); + } else { reject (body); } } @@ -56,81 +63,181 @@ var refreshTokens = function () { var generateInteractionModel = function (skill) { let result = {}; let allIntents = []; - let defaultIntents = [ - { - name: 'AMAZON.CancelIntent', - samples: [], - }, - { - name: 'AMAZON.HelpIntent', - samples: [], - }, - { - name: 'AMAZON.StopIntent', - samples: [], - }, - ]; - - /* - defaultIntents.map(intent=>{ - allIntents.push(intent); - }); - */ skill.intents.map (intent => { allIntents.push ({name: intent.intentName, samples: intent.questions}); }); - //Special Email Intents : - /* - allIntents.push ({ - name: 'EmailIntentLaunch', - slots: [], - samples: [ - 'I want to send a message', - 'I would like to send a message', - 'I would like to leave a message', - 'Leave a message', - ], - }); + //Special intent for sending message (Dialog) allIntents.push ({ - name: 'EmailIntent', + name: 'SendMessageIntent', + samples: [ + 'I would like to send a message', + 'I want to send a message', + 'Send message', + ], slots: [ { name: 'Name', type: 'AMAZON.US_FIRST_NAME', + samples: ['My name is {Name}', 'I am {Name}', '{Name}'], }, { name: 'Email', - type: 'AMAZON.LITERAL', + type: 'EmailSlot', + samples: ['My email is {Email}', '{Email}'], }, { name: 'Message', - type: 'AMAZON.LITERAL', + type: 'MessageSlot', + samples: ['{Message}'], }, - { - name: 'Data', - type: 'AMAZON.LITERAL', - }, - ], - - samples: [ - 'My name is {Name}', - 'I am {Name}', - '{exampleww at wwdwdw|Data}', - 'My email is {example at efefegedd|Email}', - 'Send replay to {example at abcdefg|Email}', - 'My message is {The quick brown fox jumps over the lazy dog.The quick brown fox jumps over the lazy dog.The quick brown fox jumps over the lazy dog.|Message}', ], }); - */ + + let customSlotTypes = [ + { + name: 'EmailSlot', + values: [ + { + id: null, + name: { + value: 'bla@bla.bla', + synonyms: [], + }, + }, + { + id: null, + name: { + value: 'bla.bla@bla.bla.bla', + synonyms: [], + }, + }, + { + id: null, + name: { + value: 'bla_bla@bla.bla', + synonyms: [], + }, + }, + ], + }, + { + name: 'MessageSlot', + values: [ + { + id: null, + name: { + value: 'Quick brown fox jumps over lazy dog', + synonyms: [], + }, + }, + { + id: null, + name: { + value: 'Quick brown fox jumps over lazy dog. Quick brown fox jumps over lazy dog.', + synonyms: [], + }, + }, + { + id: null, + name: { + value: 'Quick brown fox jumps over lazy dog. Quick brown fox jumps over lazy dog. Quick brown fox jumps over lazy dog.', + synonyms: [], + }, + }, + ], + }, + ]; + + let dialogPrompts = [ + { + id: 'Elicit.Intent-SendMessageIntent.IntentSlot-Name', + variations: [ + { + type: 'PlainText', + value: 'What is your name ?', + }, + { + type: 'PlainText', + value: 'Tell me your name', + }, + ], + }, + { + id: 'Elicit.Intent-SendMessageIntent.IntentSlot-Email', + variations: [ + { + type: 'PlainText', + value: 'What is your email ?', + }, + { + type: 'PlainText', + value: 'Tell me your email', + }, + ], + }, + { + id: 'Elicit.Intent-SendMessageIntent.IntentSlot-Message', + variations: [ + { + type: 'PlainText', + value: 'What is your message', + }, + ], + }, + ]; + + let dialogIntents = [ + { + name: 'SendMessageIntent', + confirmationRequired: false, + prompts: {}, + slots: [ + { + name: 'Name', + type: 'AMAZON.US_FIRST_NAME', + elicitationRequired: true, + confirmationRequired: false, + prompts: { + elicitation: 'Elicit.Intent-SendMessageIntent.IntentSlot-Name', + }, + }, + { + name: 'Email', + type: 'EmailSlot', + elicitationRequired: true, + confirmationRequired: false, + prompts: { + elicitation: 'Elicit.Intent-SendMessageIntent.IntentSlot-Email', + }, + }, + { + name: 'Message', + type: 'MessageSlot', + elicitationRequired: true, + confirmationRequired: false, + prompts: { + elicitation: 'Elicit.Intent-SendMessageIntent.IntentSlot-Message', + }, + }, + ], + }, + ]; + + let dialog = { + intents: dialogIntents, + }; result.interactionModel = {}; result.interactionModel.languageModel = { invocationName: skill.invocationName, + types: customSlotTypes, intents: allIntents, + prompts: dialogPrompts, + dialog: dialog, }; return JSON.stringify (result); diff --git a/backend/models/alexa.js b/backend/models/alexa.js index d95c855..ff303e6 100644 --- a/backend/models/alexa.js +++ b/backend/models/alexa.js @@ -53,19 +53,24 @@ module.exports = { }; }); + //Handler for sending message + handlers.SendMessageIntent = function () { + console.log("Dialog state : " + this.event.request.dialogState); + console.log("Intent object :"); + console.log(this.event.request.intent); + }; + //Default handlers for unknown questions and session close handlers.Unhandled = function () { this.response .speak (constants.voiceResponseStrings.QUESTION_NOT_FOUND) .listen (constants.voiceResponseStrings.GENERIC_CONTINUE); - } + }; - handlers.SessionEndedRequest = function(){ + handlers.SessionEndedRequest = function () { //We don't care for now - } - - + }; }) .catch (e => { //Something is wrong, skill is not ready, use catch-all intent to inform user From 4b594898b149fa796473b56fa4f61f5cb8216d72 Mon Sep 17 00:00:00 2001 From: GotPPay Date: Fri, 19 Jan 2018 08:00:51 +0100 Subject: [PATCH 54/83] Complete dialogtest --- backend/models/alexa.js | 52 ++++++++++++++++++++++++++++++++++++++--- 1 file changed, 49 insertions(+), 3 deletions(-) diff --git a/backend/models/alexa.js b/backend/models/alexa.js index ff303e6..4079e98 100644 --- a/backend/models/alexa.js +++ b/backend/models/alexa.js @@ -55,9 +55,55 @@ module.exports = { //Handler for sending message handlers.SendMessageIntent = function () { - console.log("Dialog state : " + this.event.request.dialogState); - console.log("Intent object :"); - console.log(this.event.request.intent); + let intent = this.event.request.intent; + + console.log ('Dialog state : ' + this.event.request.dialogState); + console.log ('Intent object :'); + console.log (intent); + + if (this.event.request.dialogState === 'STARTED') { + //Should this be in constants ? + if (!intent.slots.Name.value) { + //Name not defined yet, ask user for name + const slotToElicit = 'Name'; + const speechOutput = 'What is your name'; + const repromptSpeech = speechOutput; + this.emit ( + ':elicitSlot', + slotToElicit, + speechOutput, + repromptSpeech + ); + } else if (!intent.slots.Email.value) { + //Name not defined yet, ask user for email + const slotToElicit = 'Email'; + const speechOutput = 'What is your email'; + const repromptSpeech = speechOutput; + this.emit ( + ':elicitSlot', + slotToElicit, + speechOutput, + repromptSpeech + ); + } else if (!intent.slots.Message.value) { + const slotToElicit = 'Message'; + const speechOutput = 'What is your message'; + const repromptSpeech = speechOutput; + this.emit ( + ':elicitSlot', + slotToElicit, + speechOutput, + repromptSpeech + ); + } else { + //all slots are filled + console.log ('Name : ' + intent.slots.Name.value); + console.log ('Email : ' + intent.slots.Email.value); + console.log ('Message : ' + intents.slots.Message.value); + this.response.speak ('Ok. Someone will contact you ASAP'); + this.emit (':responseReady'); + } + } }; //Default handlers for unknown questions and session close From 2f82709f11f1ebb30b85abca2cfb7ec2e8dad3d5 Mon Sep 17 00:00:00 2001 From: GotPPay Date: Fri, 19 Jan 2018 16:29:47 +0100 Subject: [PATCH 55/83] improve dialog ; send email --- backend/config/constants.js | 2 +- backend/helpers/email.js | 58 ++++++++++++++++++++++ backend/models/alexa.js | 99 ++++++++++++++++++++++++------------- backend/package.json | 1 + 4 files changed, 126 insertions(+), 34 deletions(-) create mode 100644 backend/helpers/email.js diff --git a/backend/config/constants.js b/backend/config/constants.js index 6674956..c131195 100644 --- a/backend/config/constants.js +++ b/backend/config/constants.js @@ -27,7 +27,7 @@ constants.HTTPResultCodes = { constants.SKILL_ID_LENGTH = 24; constants.voiceResponseStrings = { - QUESTION_NOT_FOUND : 'Sorry, I didnt understan', + QUESTION_NOT_FOUND : 'Sorry, I didnt understand', GENERIC_CONTINUE : 'Would you like to continue' } diff --git a/backend/helpers/email.js b/backend/helpers/email.js new file mode 100644 index 0000000..575b75c --- /dev/null +++ b/backend/helpers/email.js @@ -0,0 +1,58 @@ +const nodemailer = require ('nodemailer'); + +module.exports = { + validateEmailFromAlexaResponse: function (email) { + //email from alexa response will contain words instead of symbols, like : + //at = @ + //underscore = _ + //dash = - + //dot = . + //TODO: This list should be longer + let transformedEmail = email + .replace (/at/gi, '@') + .replace (/underscore/gi, '_') + .replace (/dash/gi, '-') + .replace (/dot/gi, '.'); + + //Validate here with some email validation regex + + //return true if email is valid + return true; + }, + + sendEmal: function (name, fromEmail, message, toEmail) { + return new Promise ((resolve, reject) => { + let messageBody = + 'Hello. User ' + + name + + ' left you a message on Saburly service using Alexa. Content of the message : ' + + message; + + let transporter = nodemailer.createTransport ({ + host: 'smtp.yandex.com', + port: 465, + secure: true, + auth: { + user: 'saburly@yandex.com', + pass: 'WeAreSaburlyTeam', + }, + }); + + var mailOptions = { + from: fromEmail, + replyTo: fromEmail, + to: toEmail, + subject: 'Message from Saburly service', + text: messageBody, + }; + + transporter.sendMail (mailOptions, (error, info) => { + if (error) { + reject (error); + } else { + resolve (info); + } + }); + }); + }, +}; diff --git a/backend/models/alexa.js b/backend/models/alexa.js index 4079e98..25c80ac 100644 --- a/backend/models/alexa.js +++ b/backend/models/alexa.js @@ -1,9 +1,11 @@ var Alexa = require ('alexa-sdk'); const config = require ('../config/config'); var databaseHelper = require ('../helpers/database'); +var emailHelper = require ('../helpers/email'); const constants = require ('../config/constants'); var handlers = {}; +var destinationEmail; module.exports = { run: function (req, res) { @@ -30,6 +32,7 @@ module.exports = { .getSkill (config.SKILL_DB_ID) .then (activeSkill => { handlers = {}; + destinationEmail = activeSkill.contactEmail; //Handler for launch request handlers = { @@ -58,36 +61,42 @@ module.exports = { let intent = this.event.request.intent; console.log ('Dialog state : ' + this.event.request.dialogState); - console.log ('Intent object :'); console.log (intent); + //STARTED, IN_PROGRESS - if (this.event.request.dialogState === 'STARTED') { - //Should this be in constants ? - if (!intent.slots.Name.value) { - //Name not defined yet, ask user for name - const slotToElicit = 'Name'; - const speechOutput = 'What is your name'; - const repromptSpeech = speechOutput; - this.emit ( - ':elicitSlot', - slotToElicit, - speechOutput, - repromptSpeech - ); - } else if (!intent.slots.Email.value) { - //Name not defined yet, ask user for email + if (!intent.slots.Name.value) { + //Name not defined yet, ask user for name + const slotToElicit = 'Name'; + const speechOutput = 'What is your name'; + const repromptSpeech = speechOutput; + this.emit ( + ':elicitSlot', + slotToElicit, + speechOutput, + repromptSpeech + ); + } else if (!intent.slots.Email.value) { + //Name not defined yet, ask user for email + const slotToElicit = 'Email'; + const speechOutput = + 'Ok ' + intent.slots.Name.value + '. What is your email'; + const repromptSpeech = speechOutput; + this.emit ( + ':elicitSlot', + slotToElicit, + speechOutput, + repromptSpeech + ); + } else if (!intent.slots.Message.value) { + if ( + !emailHelper.validateEmailFromAlexaResponse ( + intent.slots.Email.value + ) + ) { + //Email is not valid, ask again const slotToElicit = 'Email'; - const speechOutput = 'What is your email'; - const repromptSpeech = speechOutput; - this.emit ( - ':elicitSlot', - slotToElicit, - speechOutput, - repromptSpeech - ); - } else if (!intent.slots.Message.value) { - const slotToElicit = 'Message'; - const speechOutput = 'What is your message'; + const speechOutput = + 'Sorry, that was not valid email. What is your email'; const repromptSpeech = speechOutput; this.emit ( ':elicitSlot', @@ -96,13 +105,37 @@ module.exports = { repromptSpeech ); } else { - //all slots are filled - console.log ('Name : ' + intent.slots.Name.value); - console.log ('Email : ' + intent.slots.Email.value); - console.log ('Message : ' + intents.slots.Message.value); - this.response.speak ('Ok. Someone will contact you ASAP'); - this.emit (':responseReady'); + //Email is valid + const slotToElicit = 'Message'; + const speechOutput = 'Great. What is your message'; + const repromptSpeech = speechOutput; + this.emit ( + ':elicitSlot', + slotToElicit, + speechOutput, + repromptSpeech + ); } + } else { + //all slots are filled + console.log ('Name : ' + intent.slots.Name.value); + console.log ('Email : ' + intent.slots.Email.value); + console.log ('Message : ' + intent.slots.Message.value); + emailHelper.sendEmal ( + intent.slots.Name.value, + intent.slots.Email.value, + intent.slots.Message.value, + destinationEmail + ).then(info=>{ + this.response.speak ( + 'Ok. Message sent. Someone will contact you ASAP' + ); + }).catch(error=>{ + this.response.speak ( + 'Sorry, there was a problem with sending message.' + ); + }); + this.emit (':responseReady'); } }; diff --git a/backend/package.json b/backend/package.json index b0c0319..3b48add 100644 --- a/backend/package.json +++ b/backend/package.json @@ -10,6 +10,7 @@ "express": "^4.13.0", "isomorphic-fetch": "^2.2.1", "mongodb": "^2.2.33", + "nodemailer": "^4.4.1", "request": "^2.83.0" }, "author": "Matt Kruse (http://mattkruse.com/)", From 9cff3cd9ae4f14f90857ea68a87c7c0e1dbf10f4 Mon Sep 17 00:00:00 2001 From: GotPPay Date: Fri, 19 Jan 2018 19:28:46 +0100 Subject: [PATCH 56/83] change email service --- backend/helpers/email.js | 10 +++++----- backend/models/alexa.js | 5 ++++- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/backend/helpers/email.js b/backend/helpers/email.js index 575b75c..3f92a1c 100644 --- a/backend/helpers/email.js +++ b/backend/helpers/email.js @@ -29,12 +29,12 @@ module.exports = { message; let transporter = nodemailer.createTransport ({ - host: 'smtp.yandex.com', - port: 465, - secure: true, + host: 'smtp.mail.com', + port: 587, + secure: false, auth: { - user: 'saburly@yandex.com', - pass: 'WeAreSaburlyTeam', + user: 'saburly@mail.com', + pass: 'KeepSaburly', }, }); diff --git a/backend/models/alexa.js b/backend/models/alexa.js index 25c80ac..271eb77 100644 --- a/backend/models/alexa.js +++ b/backend/models/alexa.js @@ -127,15 +127,18 @@ module.exports = { intent.slots.Message.value, destinationEmail ).then(info=>{ + console.log(info); this.response.speak ( 'Ok. Message sent. Someone will contact you ASAP' ); + this.emit (':responseReady'); }).catch(error=>{ + console.log(error); this.response.speak ( 'Sorry, there was a problem with sending message.' ); + this.emit (':responseReady'); }); - this.emit (':responseReady'); } }; From e2d76ff03ee9358727b2075d502b346df97e6dbc Mon Sep 17 00:00:00 2001 From: GotPPay Date: Fri, 19 Jan 2018 19:50:21 +0100 Subject: [PATCH 57/83] improve email preview --- backend/helpers/email.js | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/backend/helpers/email.js b/backend/helpers/email.js index 3f92a1c..749962a 100644 --- a/backend/helpers/email.js +++ b/backend/helpers/email.js @@ -23,10 +23,22 @@ module.exports = { sendEmal: function (name, fromEmail, message, toEmail) { return new Promise ((resolve, reject) => { let messageBody = - 'Hello. User ' + + 'Hello. User left you a message on Saburly service using Alexa skill. \r\nMessage : ' + + message + + '\r\nName : ' + name + - ' left you a message on Saburly service using Alexa. Content of the message : ' + - message; + '\r\nEmail : ' + + fromEmail + + '\r\nYour Saburly team'; + + let messageBodyHTML = + '

Hello. User left you a message on Saburly service using Alexa skill.


Message :

' + + message + + '


Name : ' + + name + + '


Email : ' + + fromEmail + + '


Your Saburly team'; let transporter = nodemailer.createTransport ({ host: 'smtp.mail.com', @@ -39,11 +51,12 @@ module.exports = { }); var mailOptions = { - from: fromEmail, + from: 'saburly@mail.com', replyTo: fromEmail, to: toEmail, subject: 'Message from Saburly service', text: messageBody, + html: messageBodyHTML }; transporter.sendMail (mailOptions, (error, info) => { From 0727328e58e81b279244ab171c729278d69dadb4 Mon Sep 17 00:00:00 2001 From: GotPPay Date: Fri, 19 Jan 2018 19:53:49 +0100 Subject: [PATCH 58/83] improve email preview --- backend/helpers/email.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/backend/helpers/email.js b/backend/helpers/email.js index 749962a..c2d6965 100644 --- a/backend/helpers/email.js +++ b/backend/helpers/email.js @@ -34,11 +34,11 @@ module.exports = { let messageBodyHTML = '

Hello. User left you a message on Saburly service using Alexa skill.


Message :

' + message + - '


Name : ' + + '


Name : ' + name + - '


Email : ' + + '
Email : ' + fromEmail + - '


Your Saburly team'; + '
Your Saburly team'; let transporter = nodemailer.createTransport ({ host: 'smtp.mail.com', From 00272ec67d5e7cdadf424d94572736c5f9299acd Mon Sep 17 00:00:00 2001 From: GotPPay Date: Fri, 19 Jan 2018 20:05:49 +0100 Subject: [PATCH 59/83] validate email using regex --- backend/helpers/email.js | 11 +++++++---- backend/models/alexa.js | 3 ++- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/backend/helpers/email.js b/backend/helpers/email.js index c2d6965..67d11aa 100644 --- a/backend/helpers/email.js +++ b/backend/helpers/email.js @@ -1,7 +1,7 @@ const nodemailer = require ('nodemailer'); module.exports = { - validateEmailFromAlexaResponse: function (email) { + transformEmailFromAlexaResponse: function (email) { //email from alexa response will contain words instead of symbols, like : //at = @ //underscore = _ @@ -14,10 +14,13 @@ module.exports = { .replace (/dash/gi, '-') .replace (/dot/gi, '.'); - //Validate here with some email validation regex + return transformedEmail; + }, - //return true if email is valid - return true; + isEmailValid : function(email){ + let validEmailRegex = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; + + return validEmailRegex.test(email); }, sendEmal: function (name, fromEmail, message, toEmail) { diff --git a/backend/models/alexa.js b/backend/models/alexa.js index 271eb77..3ba0c56 100644 --- a/backend/models/alexa.js +++ b/backend/models/alexa.js @@ -88,8 +88,9 @@ module.exports = { repromptSpeech ); } else if (!intent.slots.Message.value) { + intent.slots.Email.value = emailHelper.transformEmailFromAlexaResponse(intent.slots.Message.value); if ( - !emailHelper.validateEmailFromAlexaResponse ( + !emailHelper.isEmailValid ( intent.slots.Email.value ) ) { From 434d45248c99360882d6f5290fa76a1cb988e897 Mon Sep 17 00:00:00 2001 From: GotPPay Date: Fri, 19 Jan 2018 20:15:27 +0100 Subject: [PATCH 60/83] fix error in validation and undefined email value --- backend/helpers/email.js | 10 ++++++---- backend/models/alexa.js | 2 +- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/backend/helpers/email.js b/backend/helpers/email.js index 67d11aa..cf1bfb3 100644 --- a/backend/helpers/email.js +++ b/backend/helpers/email.js @@ -9,6 +9,7 @@ module.exports = { //dot = . //TODO: This list should be longer let transformedEmail = email + .replace (/\s/g, '') //remove all spaces .replace (/at/gi, '@') .replace (/underscore/gi, '_') .replace (/dash/gi, '-') @@ -17,10 +18,11 @@ module.exports = { return transformedEmail; }, - isEmailValid : function(email){ - let validEmailRegex = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; + isEmailValid: function (email) { + console.log ('Email to validate : ' + email); + let validEmailRegex = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; - return validEmailRegex.test(email); + return validEmailRegex.test (email); }, sendEmal: function (name, fromEmail, message, toEmail) { @@ -59,7 +61,7 @@ module.exports = { to: toEmail, subject: 'Message from Saburly service', text: messageBody, - html: messageBodyHTML + html: messageBodyHTML, }; transporter.sendMail (mailOptions, (error, info) => { diff --git a/backend/models/alexa.js b/backend/models/alexa.js index 3ba0c56..92f0fac 100644 --- a/backend/models/alexa.js +++ b/backend/models/alexa.js @@ -88,7 +88,7 @@ module.exports = { repromptSpeech ); } else if (!intent.slots.Message.value) { - intent.slots.Email.value = emailHelper.transformEmailFromAlexaResponse(intent.slots.Message.value); + intent.slots.Email.value = emailHelper.transformEmailFromAlexaResponse(intent.slots.Email.value); if ( !emailHelper.isEmailValid ( intent.slots.Email.value From b73086291c9c53192a6ab7d9bccb6896525d671d Mon Sep 17 00:00:00 2001 From: GotPPay Date: Fri, 19 Jan 2018 20:23:50 +0100 Subject: [PATCH 61/83] code fix and improvements --- backend/config/email.js | 14 +++++++++++ backend/helpers/email.js | 18 +++++++------- backend/models/alexa.js | 52 +++++++++++++++++++++------------------- 3 files changed, 49 insertions(+), 35 deletions(-) create mode 100644 backend/config/email.js diff --git a/backend/config/email.js b/backend/config/email.js new file mode 100644 index 0000000..89e0ddc --- /dev/null +++ b/backend/config/email.js @@ -0,0 +1,14 @@ +var config = {}; + +config.PORT = 587; +config.SMTP_HOST = 'smtp.mail.com'; +config.SECURE = false; +config.AUTH = { + user: 'saburly@mail.com', + pass: 'KeepSaburly', +}; + +config.FROM_EMAIL = 'saburly@mail.com'; +config.SUBJECT = 'Message from Saburly service'; + +module.exports = config; diff --git a/backend/helpers/email.js b/backend/helpers/email.js index cf1bfb3..c70eadc 100644 --- a/backend/helpers/email.js +++ b/backend/helpers/email.js @@ -1,4 +1,5 @@ const nodemailer = require ('nodemailer'); +const emailConfig = require('../config/email'); module.exports = { transformEmailFromAlexaResponse: function (email) { @@ -43,23 +44,20 @@ module.exports = { name + '
Email : ' + fromEmail + - '
Your Saburly team'; + '

Your Saburly team'; let transporter = nodemailer.createTransport ({ - host: 'smtp.mail.com', - port: 587, - secure: false, - auth: { - user: 'saburly@mail.com', - pass: 'KeepSaburly', - }, + host: emailConfig.SMTP_HOST, + port: emailConfig.PORT, + secure: emailConfig.SECURE, + auth: emailConfig.AUTH, }); var mailOptions = { - from: 'saburly@mail.com', + from: emailConfig.FROM_EMAIL, replyTo: fromEmail, to: toEmail, - subject: 'Message from Saburly service', + subject: emailConfig.SUBJECT, text: messageBody, html: messageBodyHTML, }; diff --git a/backend/models/alexa.js b/backend/models/alexa.js index 92f0fac..da282f3 100644 --- a/backend/models/alexa.js +++ b/backend/models/alexa.js @@ -88,12 +88,10 @@ module.exports = { repromptSpeech ); } else if (!intent.slots.Message.value) { - intent.slots.Email.value = emailHelper.transformEmailFromAlexaResponse(intent.slots.Email.value); - if ( - !emailHelper.isEmailValid ( - intent.slots.Email.value - ) - ) { + intent.slots.Email.value = emailHelper.transformEmailFromAlexaResponse ( + intent.slots.Email.value + ); + if (!emailHelper.isEmailValid (intent.slots.Email.value)) { //Email is not valid, ask again const slotToElicit = 'Email'; const speechOutput = @@ -103,7 +101,8 @@ module.exports = { ':elicitSlot', slotToElicit, speechOutput, - repromptSpeech + repromptSpeech, + intent ); } else { //Email is valid @@ -122,24 +121,27 @@ module.exports = { console.log ('Name : ' + intent.slots.Name.value); console.log ('Email : ' + intent.slots.Email.value); console.log ('Message : ' + intent.slots.Message.value); - emailHelper.sendEmal ( - intent.slots.Name.value, - intent.slots.Email.value, - intent.slots.Message.value, - destinationEmail - ).then(info=>{ - console.log(info); - this.response.speak ( - 'Ok. Message sent. Someone will contact you ASAP' - ); - this.emit (':responseReady'); - }).catch(error=>{ - console.log(error); - this.response.speak ( - 'Sorry, there was a problem with sending message.' - ); - this.emit (':responseReady'); - }); + emailHelper + .sendEmal ( + intent.slots.Name.value, + intent.slots.Email.value, + intent.slots.Message.value, + destinationEmail + ) + .then (info => { + console.log (info); + this.response.speak ( + 'Ok. Message sent. Someone will contact you ASAP' + ); + this.emit (':responseReady'); + }) + .catch (error => { + console.log (error); + this.response.speak ( + 'Sorry, there was a problem with sending message.' + ); + this.emit (':responseReady'); + }); } }; From e2d648980b42962fa454719d974fcec8bf6b41e3 Mon Sep 17 00:00:00 2001 From: GotPPay Date: Fri, 19 Jan 2018 20:28:26 +0100 Subject: [PATCH 62/83] fix code --- backend/helpers/email.js | 1 + backend/models/alexa.js | 3 +-- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/helpers/email.js b/backend/helpers/email.js index c70eadc..76e109a 100644 --- a/backend/helpers/email.js +++ b/backend/helpers/email.js @@ -28,6 +28,7 @@ module.exports = { sendEmal: function (name, fromEmail, message, toEmail) { return new Promise ((resolve, reject) => { + fromEmail = this.transformEmailFromAlexaResponse(fromEmail); let messageBody = 'Hello. User left you a message on Saburly service using Alexa skill. \r\nMessage : ' + message + diff --git a/backend/models/alexa.js b/backend/models/alexa.js index da282f3..4074784 100644 --- a/backend/models/alexa.js +++ b/backend/models/alexa.js @@ -101,8 +101,7 @@ module.exports = { ':elicitSlot', slotToElicit, speechOutput, - repromptSpeech, - intent + repromptSpeech ); } else { //Email is valid From 48578d3ffe8e99ba449768da6a06a90289c79fc6 Mon Sep 17 00:00:00 2001 From: GotPPay Date: Fri, 19 Jan 2018 22:42:37 +0100 Subject: [PATCH 63/83] Change UI --- README.md | 2 +- web/src/App.js | 349 ++++++++++++++++++---------- web/src/components/IntentDetails.js | 22 +- web/src/config/constants.js | 1 + 4 files changed, 252 insertions(+), 122 deletions(-) diff --git a/README.md b/README.md index caec0bf..fc36472 100644 --- a/README.md +++ b/README.md @@ -67,7 +67,7 @@ Prerequests for step 3 (run on server): requires running mongodb service Database (tellall) with collection (skill_list) - * Insert dummy skill with : db.skill_list.insert({"skillID" : "amzn1.ask.skill.efbf0564-a732-4ba9-958f-57939138adae", "intents" : [ { "intentName" : "GetFirstQuestion", "questions" : [ "tell me something about projects", "tell me all about projects" ], "answer" : "blablabla bla bla" }, { "intentName" : "GetThirdQuestion", "questions" : [ "Give me third question" ], "answer" : "This is answer to the third question" } ], "invocationName" : "Saburly", "invocationAnswer" : "We are Saburly team one" }) + * Insert dummy skill with : db.skill_list.insert({"skillID" : "amzn1.ask.skill.efbf0564-a732-4ba9-958f-57939138adae", "intents" : [ { "intentName" : "GetFirstQuestion", "questionExplanation" : "", "questions" : [ "tell me something about projects", "tell me all about projects" ], "answer" : "blablabla bla bla" }, { "intentName" : "GetThirdQuestion", "questionExplanation" : "", "questions" : [ "Give me third question" ], "answer" : "This is answer to the third question" } ], "invocationName" : "Saburly", "invocationAnswer" : "We are Saburly team one" }) *obtain _id and change in web/src/App.js, and also skill_db_id in backend/config.js *enter web/ dir and run "npm run build" diff --git a/web/src/App.js b/web/src/App.js index 663d0ec..9f46a99 100644 --- a/web/src/App.js +++ b/web/src/App.js @@ -1,4 +1,4 @@ -import React, { Component } from 'react'; +import React, {Component} from 'react'; import './css/App.css'; import './css/popup.css'; import IntentList from './components/IntentList'; @@ -6,90 +6,117 @@ import IntentDetails from './components/IntentDetails'; import LaunchRequest from './components/LaunchRequest'; import Contact from './components/Contact'; import Popup from 'react-popup'; -import {getSkill, updateSkill} from './lib/api' +import {getSkill, updateSkill} from './lib/api'; import { - NEW_INTENT_SELECTED_INDEX, - LAUNCH_REQUEST_SELECTED_INDEX, - CONTACT_SELECTED_INDEX, - RESULT_CODES} from './config/constants' + NEW_INTENT_SELECTED_INDEX, + LAUNCH_REQUEST_SELECTED_INDEX, + CONTACT_SELECTED_INDEX, + RESULT_CODES, +} from './config/constants'; class App extends Component { + constructor (props) { + super (props); - constructor(props){ - super(props); + this.state = { + _id: '5a232fb86ce046c749739455', + skillID: '', + skillName: '', + invocationName: 'Saburly', + invocationAnswer: 'We are saburly', + allIntents: [], + selectedIntent: { + intentName: '', + intentExplanation: '', + questions: [''], + answer: '', + }, + selectedIndex: NEW_INTENT_SELECTED_INDEX, + contactEmail: '', + waiting: false, + }; - this.state={_id:'5a232fb86ce046c749739455', - skillID:'', - skillName:'', - invocationName:'Saburly', - invocationAnswer:'We are saburly', - allIntents:[], - selectedIntent: {intentName:'',questions:[''],answer:''}, - selectedIndex:NEW_INTENT_SELECTED_INDEX, - contactEmail:'', - waiting: false - }; + getSkill (this.state._id).then (l => l.json ()).then (result => { + if (result === undefined) return; + this.setState ({ + skillID: result.skillID, + skillName: result.skillName, + invocationName: result.invocationName, + invocationAnswer: result.invocationAnswer, + allIntents: result.intents, + contactEmail: result.contactEmail, + }); + }); - getSkill(this.state._id).then(l=>l.json()).then(result=>{ - if (result===undefined) return; - this.setState({ skillID:result.skillID,skillName:result.skillName, invocationName: result.invocationName, - invocationAnswer: result.invocationAnswer, - allIntents: result.intents, contactEmail: result.contactEmail}) - }) - - this.handleIntentClick = this.handleIntentClick.bind(this); - this.handleLaunchRequestClick = this.handleLaunchRequestClick.bind(this); - this.handleDeleteIntentClick = this.handleDeleteIntentClick.bind(this); - this.handleSaveIntentClick = this.handleSaveIntentClick.bind(this); - this.handleAddIntentClick = this.handleAddIntentClick.bind(this); - this.handleSaveLaunchRequestClick = this.handleSaveLaunchRequestClick.bind(this); - this.createSkill = this.createSkill.bind(this); - this.sendSkill = this.sendSkill.bind(this); - this.handleContactClick = this.handleContactClick.bind(this); - this.handleSaveEmailClick = this.handleSaveEmailClick.bind(this); + this.handleIntentClick = this.handleIntentClick.bind (this); + this.handleLaunchRequestClick = this.handleLaunchRequestClick.bind (this); + this.handleDeleteIntentClick = this.handleDeleteIntentClick.bind (this); + this.handleSaveIntentClick = this.handleSaveIntentClick.bind (this); + this.handleAddIntentClick = this.handleAddIntentClick.bind (this); + this.handleSaveLaunchRequestClick = this.handleSaveLaunchRequestClick.bind ( + this + ); + this.createSkill = this.createSkill.bind (this); + this.sendSkill = this.sendSkill.bind (this); + this.handleContactClick = this.handleContactClick.bind (this); + this.handleSaveEmailClick = this.handleSaveEmailClick.bind (this); } - render() { + render () { let rightPanel; switch (this.state.selectedIndex) { case LAUNCH_REQUEST_SELECTED_INDEX: - rightPanel = ; + rightPanel = ( + + ); break; case CONTACT_SELECTED_INDEX: - rightPanel = ; + rightPanel = ( + + ); break; default: - rightPanel = ; + rightPanel = ( + + ); } - return( + return (
- +

Tell All

- - + + {rightPanel}
); } - createSkill(intents, name, answer, email, updateOnAmazon){ + createSkill (intents, name, answer, email, updateOnAmazon) { return { _id: this.state._id, skillID: this.state.skillID, @@ -97,97 +124,181 @@ class App extends Component { invocationName: name, invocationAnswer: answer, contactEmail: email, - updateOnAmazon: updateOnAmazon + updateOnAmazon: updateOnAmazon, }; } - handleIntentClick(selectedIntent, index){ - this.setState({selectedIntent:selectedIntent, selectedIndex: index, launchRequest:false}); + handleIntentClick (selectedIntent, index) { + this.setState ({ + selectedIntent: selectedIntent, + selectedIndex: index, + launchRequest: false, + }); } - handleLaunchRequestClick(){ - this.setState({selectedIndex: LAUNCH_REQUEST_SELECTED_INDEX}); + handleLaunchRequestClick () { + this.setState ({selectedIndex: LAUNCH_REQUEST_SELECTED_INDEX}); } - handleContactClick(){ - this.setState({selectedIndex: CONTACT_SELECTED_INDEX}) + handleContactClick () { + this.setState ({selectedIndex: CONTACT_SELECTED_INDEX}); } - handleSaveLaunchRequestClick(name, answer){ - this.setState({waiting:true, invocationName:name, invocationAnswer: answer}); - this.sendSkill(this.state.allIntents,true,{waiting:false},{waiting:false},name,answer,this.state.contactEmail,true); + handleSaveLaunchRequestClick (name, answer) { + this.setState ({ + waiting: true, + invocationName: name, + invocationAnswer: answer, + }); + this.sendSkill ( + this.state.allIntents, + true, + {waiting: false}, + {waiting: false}, + name, + answer, + this.state.contactEmail, + true + ); } - handleSaveEmailClick(email){ - this.setState({waiting:true}); - this.sendSkill(this.state.allIntents,true,{contactEmail: email, waiting:false},{waiting:false},this.state.invocationName,this.state.invocationAnswer,email,false); + handleSaveEmailClick (email) { + this.setState ({waiting: true}); + this.sendSkill ( + this.state.allIntents, + true, + {contactEmail: email, waiting: false}, + {waiting: false}, + this.state.invocationName, + this.state.invocationAnswer, + email, + false + ); } - handleDeleteIntentClick(selectedIntent){ + handleDeleteIntentClick (selectedIntent) { let id = -1; //TODO : Change comparsion method ! Same object with different proeprty sorting will not be same string - this.state.allIntents.map((intent,index)=>{ - if ((id===-1) && (JSON.stringify(selectedIntent)===JSON.stringify(intent))) - id = index; + this.state.allIntents.map ((intent, index) => { + if ( + id === -1 && + JSON.stringify (selectedIntent) === JSON.stringify (intent) + ) + id = index; }); - if (id!==-1){ - try{ - let newAllIntentsJSON = JSON.stringify(this.state.allIntents); - let newAllIntents = JSON.parse(newAllIntentsJSON); - newAllIntents.splice(id,1); - this.setState({waiting:true}); + if (id !== -1) { + try { + let newAllIntentsJSON = JSON.stringify (this.state.allIntents); + let newAllIntents = JSON.parse (newAllIntentsJSON); + newAllIntents.splice (id, 1); + this.setState ({waiting: true}); - let newState = {allIntents: newAllIntents, selectedIntent: {intentName:'', questions:[''],answer:''}, waiting:false}; - this.sendSkill(newAllIntents,true,newState,{waiting:false},this.state.invocationName,this.state.invocationAnswer,this.state.contactEmail,true); - - }catch(e){ - console.log("error : " + e); + let newState = { + allIntents: newAllIntents, + selectedIntent: {intentName: '', questions: [''], answer: ''}, + waiting: false, + }; + this.sendSkill ( + newAllIntents, + true, + newState, + {waiting: false}, + this.state.invocationName, + this.state.invocationAnswer, + this.state.contactEmail, + true + ); + } catch (e) { + console.log ('error : ' + e); } } } - - handleSaveIntentClick(selectedIntent){ - let newAllIntentsJSON = JSON.stringify(this.state.allIntents); - let newAllIntents = JSON.parse(newAllIntentsJSON); + handleSaveIntentClick (selectedIntent) { + let newAllIntentsJSON = JSON.stringify (this.state.allIntents); + let newAllIntents = JSON.parse (newAllIntentsJSON); let newState = null; - if (this.state.selectedIndex === NEW_INTENT_SELECTED_INDEX){ + if (this.state.selectedIndex === NEW_INTENT_SELECTED_INDEX) { //new intent - newAllIntents.push(selectedIntent); - newState = {allIntents: newAllIntents, selectedIntent: selectedIntent, selectedIndex: newAllIntents.length-1, waiting:false}; - }else{ + newAllIntents.push (selectedIntent); + newState = { + allIntents: newAllIntents, + selectedIntent: selectedIntent, + selectedIndex: newAllIntents.length - 1, + waiting: false, + }; + } else { newAllIntents[this.state.selectedIndex] = selectedIntent; - newState = {allIntents: newAllIntents, selectedIntent: selectedIntent, waiting: false}; + newState = { + allIntents: newAllIntents, + selectedIntent: selectedIntent, + waiting: false, + }; } - this.setState({waiting:true}); - this.sendSkill(newAllIntents, true, newState, {waiting:false}, this.state.invocationName,this.state.invocationAnswer,this.state.contactEmail, true); + this.setState ({waiting: true}); + this.sendSkill ( + newAllIntents, + true, + newState, + {waiting: false}, + this.state.invocationName, + this.state.invocationAnswer, + this.state.contactEmail, + true + ); } - handleAddIntentClick(){ - this.setState({allIntents: this.state.allIntents, selectedIndex: NEW_INTENT_SELECTED_INDEX,launchRequest:false,selectedIntent: {intentName:'',questions:[''], answer:''}}); + handleAddIntentClick () { + this.setState ({ + allIntents: this.state.allIntents, + selectedIndex: NEW_INTENT_SELECTED_INDEX, + launchRequest: false, + selectedIntent: {intentName: '', questions: [''], answer: ''}, + }); } - sendSkill(newAllIntents, showPopUp, resolveState, rejectState, newName, newAnswer, email, updateOnAmazon){ - return new Promise((resolve,reject)=>{ - updateSkill(this.createSkill(newAllIntents,newName,newAnswer,email,updateOnAmazon)).then(l=>l.json()).then(result=>{ - if (result.result !== RESULT_CODES.OK){ - console.log(result.result); - if (showPopUp) Popup.alert('Model was not saved. Please try again'); - this.setState(rejectState); - //reject('Error code : ' + jResult.result); - }else{ - if (showPopUp) Popup.alert('Saved'); - this.setState(resolveState); - resolve(); - } - }).catch(e=>{ - console.log('error : ' + e); - if (showPopUp) Popup.alert('Model was not saved. Please try again'); - this.setState(rejectState); - //reject(e); - }); + sendSkill ( + newAllIntents, + showPopUp, + resolveState, + rejectState, + newName, + newAnswer, + email, + updateOnAmazon + ) { + return new Promise ((resolve, reject) => { + updateSkill ( + this.createSkill ( + newAllIntents, + newName, + newAnswer, + email, + updateOnAmazon + ) + ) + .then (l => l.json ()) + .then (result => { + if (result.result !== RESULT_CODES.OK) { + console.log (result); + if (showPopUp) + Popup.alert ('Model was not saved. Please try again'); + this.setState (rejectState); + //reject('Error code : ' + jResult.result); + } else { + if (showPopUp) Popup.alert ('Saved'); + this.setState (resolveState); + resolve (); + } + }) + .catch (e => { + console.log ('error : ' + e); + if (showPopUp) Popup.alert ('Model was not saved. Please try again'); + this.setState (rejectState); + //reject(e); + }); }); } } diff --git a/web/src/components/IntentDetails.js b/web/src/components/IntentDetails.js index e5c6066..8e76100 100644 --- a/web/src/components/IntentDetails.js +++ b/web/src/components/IntentDetails.js @@ -1,7 +1,8 @@ import React, { Component } from 'react'; import {Button, SVGIcon, TextField} from 'react-md'; import '../css/components/IntentDetails.css'; -import {QUESTION_MAX_LENGTH, ANSWER_MAX_LENGTH, INTENT_NAME_MAX_LENGTH} from '../config/constants'; +import '../css/Common.css'; +import {QUESTION_MAX_LENGTH, ANSWER_MAX_LENGTH, INTENT_NAME_MAX_LENGTH, INTENT_EXPLANATION_MAX_LENGTH} from '../config/constants'; class IntentDetails extends Component { constructor(props){ @@ -14,6 +15,7 @@ class IntentDetails extends Component { this.handleQuestionEdit = this.handleQuestionEdit.bind(this); this.handleAnswerEdit = this.handleAnswerEdit.bind(this); this.handleIntentNameEdit = this.handleIntentNameEdit.bind(this); + this.handleIntentExplanationEdit = this.handleIntentExplanationEdit.bind(this); } componentWillReceiveProps(props){ @@ -24,10 +26,19 @@ class IntentDetails extends Component { return (
+
In introduction, Alexa will help users to ask her the right questions about your business. For Example, she will say : "To ask us about our services, say : What do you do ? ". What do you do ? is defined in question field. Alexa will use first variation of question in intro.
+ +
Date: Fri, 19 Jan 2018 22:54:50 +0100 Subject: [PATCH 64/83] fix reverting skill on failed update --- backend/config/constants.js | 2 +- backend/controllers/skill.js | 86 +++++++++++++++++++++++++----------- 2 files changed, 61 insertions(+), 27 deletions(-) diff --git a/backend/config/constants.js b/backend/config/constants.js index c131195..b60f24b 100644 --- a/backend/config/constants.js +++ b/backend/config/constants.js @@ -17,7 +17,7 @@ constants.apiResultCodes = { AMAZON_FAIL:2, //amazon api doesn't work DATABASE_ERROR:3, NO_SKILL:4, - INCONSISTEN_STATE:5, + INCONSISTENT_STATE:5, } constants.HTTPResultCodes = { diff --git a/backend/controllers/skill.js b/backend/controllers/skill.js index 6cc36e7..a0c2f5f 100644 --- a/backend/controllers/skill.js +++ b/backend/controllers/skill.js @@ -41,6 +41,7 @@ router.put ('/:id', bodyParser.json (), async (req, res, next) => { .then (() => { //Ok, done, now update skill on Amazon (if needed) if (updateOnAmazon) { + //We need to update skill on Amazon amazonHelper .updateSkill (skill) .then (amazonResult => { @@ -51,45 +52,78 @@ router.put ('/:id', bodyParser.json (), async (req, res, next) => { res.json ({result: constants.apiResultCodes.OK, message: ''}); alexa.updateModel (); } else { - res.status(constants.HTTPResultCodes.INTERNAL_SERVER_ERROR).json ({ - result: constants.apiResultCodes.AMAZON_ERROR, - message: amazonResult, - }); + //Update on amazon failed, revert changes in database and send error to user + databaseHelper + .updateSkill (id, currentSkillState) + .then (() => { + res + .status ( + constants.HTTPResultCodes.INTERNAL_SERVER_ERROR + ) + .json ({ + result: constants.apiResultCodes.AMAZON_ERROR, + message: amazonResult, + }); + }) + .catch (() => { + //This should never happen, something is seriously wrong, like no database connection + res + .status ( + constants.HTTPResultCodes.INTERNAL_SERVER_ERROR + ) + .json ({ + result: constants.apiResultCodes.INCONSISTENT_STATE, + message: '', + }); + }); } }) .catch (e => { - res.status(constants.HTTPResultCodes.INTERNAL_SERVER_ERROR).json ({ - result: constants.apiResultCodes.AMAZON_FAIL, - message: e, - }); + //Update on amazon failed, revert changes in database and send error to user + databaseHelper + .updateSkill (id, currentSkillState) + .then (() => { + res + .status (constants.HTTPResultCodes.INTERNAL_SERVER_ERROR) + .json ({ + result: constants.apiResultCodes.AMAZON_FAIL, + message: e, + }); + }) + .catch (() => { + //This should never happen, something is seriously wrong, like no database connection + res + .status (constants.HTTPResultCodes.INTERNAL_SERVER_ERROR) + .json ({ + result: constants.apiResultCodes.INCONSISTENT_STATE, + message: '', + }); + }); }); - }else{ + } else { + //No need to update on Amazon, tell to user it's ok res.json ({result: constants.apiResultCodes.OK, message: ''}); alexa.updateModel (); } }) .catch (() => { - //Update in database didn't go well, revert changes - databaseHelper - .updateSkill (id, currentSkillState) - .then (() => { - res.status(constants.HTTPResultCodes.INTERNAL_SERVER_ERROR).json ({ - result: constants.apiResultCodes.DATABASE_ERROR, - message: '', - }); - }) - .catch (() => { - //This should never happen, something is seriously wrong, like no database connection - res.status(constants.HTTPResultCodes.INTERNAL_SERVER_ERROR).json ({ - result: constants.apiResultCodes.INCONSISTEN_STATE, - message: '', - }); - }); + //Update in database didn't go well, no need to revert since it failed to write in the first place + //just send error to user + res + .status ( + constants.HTTPResultCodes.INTERNAL_SERVER_ERROR + ) + .json ({ + result: constants.apiResultCodes.DATABASE_ERROR, + message: '', + }); }); }) .catch (e => { //I don't know why, but something went wrong, possibly ID of skill is wrong, doesn't exist in DB - res.status(constants.HTTPResultCodes.INTERNAL_SERVER_ERROR).json ({result: constants.apiResultCodes.NO_SKILL, message: ''}); + res + .status (constants.HTTPResultCodes.INTERNAL_SERVER_ERROR) + .json ({result: constants.apiResultCodes.NO_SKILL, message: ''}); }); }); From d8799fa40d32fb80a9d63d46dc20c0bf0338f9ea Mon Sep 17 00:00:00 2001 From: GotPPay Date: Fri, 19 Jan 2018 23:26:59 +0100 Subject: [PATCH 65/83] fix JSON model --- backend/helpers/amazon.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/backend/helpers/amazon.js b/backend/helpers/amazon.js index 6d1b553..2839f40 100644 --- a/backend/helpers/amazon.js +++ b/backend/helpers/amazon.js @@ -236,10 +236,12 @@ var generateInteractionModel = function (skill) { invocationName: skill.invocationName, types: customSlotTypes, intents: allIntents, - prompts: dialogPrompts, - dialog: dialog, }; + result.prompts = dialogPrompts; + result.dialog = dialog; + + console.log(JSON.stringify(result)); return JSON.stringify (result); }; From 085a0324b3a9681e0be11e1df3a001b42ce20386 Mon Sep 17 00:00:00 2001 From: GotPPay Date: Mon, 22 Jan 2018 20:12:16 +0100 Subject: [PATCH 66/83] . --- backend/helpers/amazon.js | 29 ++++++++++++++++++++--------- 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/backend/helpers/amazon.js b/backend/helpers/amazon.js index 2839f40..fa414ac 100644 --- a/backend/helpers/amazon.js +++ b/backend/helpers/amazon.js @@ -64,12 +64,25 @@ var generateInteractionModel = function (skill) { let result = {}; let allIntents = []; + allIntents.push({ + "name": "AMAZON.CancelIntent", + "samples": [] + }, + { + "name": "AMAZON.HelpIntent", + "samples": [] + }, + { + "name": "AMAZON.StopIntent", + "samples": [] + },); + skill.intents.map (intent => { - allIntents.push ({name: intent.intentName, samples: intent.questions}); + allIntents.push ({name: intent.intentName, samples: intent.questions, slots:[]}); }); //Special intent for sending message (Dialog) - +/* allIntents.push ({ name: 'SendMessageIntent', samples: [ @@ -95,7 +108,7 @@ var generateInteractionModel = function (skill) { }, ], }); - +*/ let customSlotTypes = [ { name: 'EmailSlot', @@ -230,16 +243,14 @@ var generateInteractionModel = function (skill) { intents: dialogIntents, }; - result.interactionModel = {}; - - result.interactionModel.languageModel = { + result.languageModel = { invocationName: skill.invocationName, - types: customSlotTypes, + //types: customSlotTypes, intents: allIntents, }; - result.prompts = dialogPrompts; - result.dialog = dialog; + //result.prompts = dialogPrompts; + //result.dialog = dialog; console.log(JSON.stringify(result)); return JSON.stringify (result); From a00859c5944127e932add03841c818c03b2b0535 Mon Sep 17 00:00:00 2001 From: GotPPay Date: Mon, 22 Jan 2018 22:34:13 +0100 Subject: [PATCH 67/83] fix InteractionModel generator --- backend/controllers/skill.js | 1 + backend/helpers/amazon.js | 43 ++++++++++++++---------------------- 2 files changed, 17 insertions(+), 27 deletions(-) diff --git a/backend/controllers/skill.js b/backend/controllers/skill.js index a0c2f5f..921e45b 100644 --- a/backend/controllers/skill.js +++ b/backend/controllers/skill.js @@ -79,6 +79,7 @@ router.put ('/:id', bodyParser.json (), async (req, res, next) => { } }) .catch (e => { + console.log(e); //Update on amazon failed, revert changes in database and send error to user databaseHelper .updateSkill (id, currentSkillState) diff --git a/backend/helpers/amazon.js b/backend/helpers/amazon.js index fa414ac..486f1ab 100644 --- a/backend/helpers/amazon.js +++ b/backend/helpers/amazon.js @@ -64,25 +64,12 @@ var generateInteractionModel = function (skill) { let result = {}; let allIntents = []; - allIntents.push({ - "name": "AMAZON.CancelIntent", - "samples": [] - }, - { - "name": "AMAZON.HelpIntent", - "samples": [] - }, - { - "name": "AMAZON.StopIntent", - "samples": [] - },); - skill.intents.map (intent => { - allIntents.push ({name: intent.intentName, samples: intent.questions, slots:[]}); + allIntents.push ({name: intent.intentName, samples: intent.questions}); }); //Special intent for sending message (Dialog) -/* + allIntents.push ({ name: 'SendMessageIntent', samples: [ @@ -108,7 +95,9 @@ var generateInteractionModel = function (skill) { }, ], }); -*/ + + + let customSlotTypes = [ { name: 'EmailSlot', @@ -163,7 +152,7 @@ var generateInteractionModel = function (skill) { ], }, ]; - + let dialogPrompts = [ { id: 'Elicit.Intent-SendMessageIntent.IntentSlot-Name', @@ -201,7 +190,7 @@ var generateInteractionModel = function (skill) { ], }, ]; - + let dialogIntents = [ { name: 'SendMessageIntent', @@ -239,24 +228,24 @@ var generateInteractionModel = function (skill) { }, ]; - let dialog = { - intents: dialogIntents, - }; + result.interactionModel = {}; - result.languageModel = { + result.interactionModel.languageModel = { invocationName: skill.invocationName, - //types: customSlotTypes, + types: customSlotTypes, intents: allIntents, }; - //result.prompts = dialogPrompts; - //result.dialog = dialog; + result.interactionModel.prompts = dialogPrompts; + result.interactionModel.dialog = {}; + result.interactionModel.dialog.intents = dialogIntents; - console.log(JSON.stringify(result)); return JSON.stringify (result); }; var uploadSkill = function (skill) { + let generatedInteractionModel = generateInteractionModel(skill); + console.log(generatedInteractionModel); return fetch ( `https://api.amazonalexa.com/v0/skills/${skill.skillID}/interactionModel/locales/en-US`, { @@ -264,7 +253,7 @@ var uploadSkill = function (skill) { headers: { Authorization: config.TOKEN, }, - body: generateInteractionModel (skill), + body: generatedInteractionModel, } ); }; From af19108e9ced5d214b649cc615e2844054d34d73 Mon Sep 17 00:00:00 2001 From: GotPPay Date: Mon, 22 Jan 2018 22:34:58 +0100 Subject: [PATCH 68/83] fix InteractionModel generator --- backend/controllers/skill.js | 1 - backend/helpers/amazon.js | 1 - 2 files changed, 2 deletions(-) diff --git a/backend/controllers/skill.js b/backend/controllers/skill.js index 921e45b..a0c2f5f 100644 --- a/backend/controllers/skill.js +++ b/backend/controllers/skill.js @@ -79,7 +79,6 @@ router.put ('/:id', bodyParser.json (), async (req, res, next) => { } }) .catch (e => { - console.log(e); //Update on amazon failed, revert changes in database and send error to user databaseHelper .updateSkill (id, currentSkillState) diff --git a/backend/helpers/amazon.js b/backend/helpers/amazon.js index 486f1ab..9cc046f 100644 --- a/backend/helpers/amazon.js +++ b/backend/helpers/amazon.js @@ -245,7 +245,6 @@ var generateInteractionModel = function (skill) { var uploadSkill = function (skill) { let generatedInteractionModel = generateInteractionModel(skill); - console.log(generatedInteractionModel); return fetch ( `https://api.amazonalexa.com/v0/skills/${skill.skillID}/interactionModel/locales/en-US`, { From 2c6953fe97721bc16086dfea7bda55c994af836b Mon Sep 17 00:00:00 2001 From: GotPPay Date: Mon, 22 Jan 2018 23:35:25 +0100 Subject: [PATCH 69/83] list all questions on Launch --- backend/models/alexa.js | 32 ++++++++++++++++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/backend/models/alexa.js b/backend/models/alexa.js index 4074784..0ff79d8 100644 --- a/backend/models/alexa.js +++ b/backend/models/alexa.js @@ -34,14 +34,42 @@ module.exports = { handlers = {}; destinationEmail = activeSkill.contactEmail; + //Defaul Amazon handlers (some of them) + + handlers.NewSession = function () { + this.attributes['WantToHearQuestions'] = false; + }; + + handlers['AMAZON.YesIntent'] = function () { + if (this.attributes['WantToHearQuestions']) { + let listOfPossibleQuestions = ''; + activeSkill.intents.map (intent => { + if (intent.questions.length > 0) { + listOfPossibleQuestions += + intent.intentExplanation + + intent.questions[0] + + ''; + } + }); + this.response + .speak (listOfPossibleQuestions) + .listen(listOfPossibleQuestions); + this.emit(':responseReady'); + } + }; + //Handler for launch request handlers = { LaunchRequest: function () { this.response .speak (activeSkill.invocationAnswer) + .speak ( + 'Would you like to hear questions that you can ask me ?' + ) .listen (constants.voiceResponseStrings.GENERIC_CONTINUE); //Phrase from listen doesn't work !!! - //TODO : Maybe to ask user does he want to hear possible intents - //For this functionality, we need explanation text for each intent (Question) + + this.attributes['WantToHearQuestions'] = true; + this.emit (':responseReady'); }, }; From d58d4b89e3b33342b292d9fe85e3171b97e9c55c Mon Sep 17 00:00:00 2001 From: GotPPay Date: Tue, 23 Jan 2018 00:08:39 +0100 Subject: [PATCH 70/83] experiment with yes intent --- backend/models/alexa.js | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/backend/models/alexa.js b/backend/models/alexa.js index 0ff79d8..271b528 100644 --- a/backend/models/alexa.js +++ b/backend/models/alexa.js @@ -12,7 +12,6 @@ module.exports = { // Build the context manually, because Amazon Lambda is missing var context = { succeed: function (result) { - console.log (result); res.json (result); }, fail: function (error) { @@ -34,6 +33,17 @@ module.exports = { handlers = {}; destinationEmail = activeSkill.contactEmail; + let listOfPossibleQuestions = ''; + activeSkill.intents.map (intent => { + if (intent.questions.length > 0) { + listOfPossibleQuestions += + intent.intentExplanation + + intent.questions[0] + + ''; + } + }); + console.log(listOfPossibleQuestions); + //Defaul Amazon handlers (some of them) handlers.NewSession = function () { @@ -41,16 +51,8 @@ module.exports = { }; handlers['AMAZON.YesIntent'] = function () { + console.log("yes intent"); if (this.attributes['WantToHearQuestions']) { - let listOfPossibleQuestions = ''; - activeSkill.intents.map (intent => { - if (intent.questions.length > 0) { - listOfPossibleQuestions += - intent.intentExplanation + - intent.questions[0] + - ''; - } - }); this.response .speak (listOfPossibleQuestions) .listen(listOfPossibleQuestions); @@ -63,9 +65,6 @@ module.exports = { LaunchRequest: function () { this.response .speak (activeSkill.invocationAnswer) - .speak ( - 'Would you like to hear questions that you can ask me ?' - ) .listen (constants.voiceResponseStrings.GENERIC_CONTINUE); //Phrase from listen doesn't work !!! this.attributes['WantToHearQuestions'] = true; From b1a853c3631e46ecddb80b2ea6359ad17f047e0c Mon Sep 17 00:00:00 2001 From: GotPPay Date: Tue, 23 Jan 2018 00:20:50 +0100 Subject: [PATCH 71/83] experiment with Yes and No intents --- backend/helpers/amazon.js | 23 +++++++++++++++++------ backend/models/alexa.js | 15 +++++++++++---- 2 files changed, 28 insertions(+), 10 deletions(-) diff --git a/backend/helpers/amazon.js b/backend/helpers/amazon.js index 9cc046f..f280ed4 100644 --- a/backend/helpers/amazon.js +++ b/backend/helpers/amazon.js @@ -64,12 +64,25 @@ var generateInteractionModel = function (skill) { let result = {}; let allIntents = []; + //Special Amazon intent + + allIntents.push ( + { + name: 'AMAZON.YesIntent', + samples: [], + }, + { + name: 'AMAZON.NoIntent', + samples: [], + } + ); + skill.intents.map (intent => { allIntents.push ({name: intent.intentName, samples: intent.questions}); }); //Special intent for sending message (Dialog) - + allIntents.push ({ name: 'SendMessageIntent', samples: [ @@ -95,9 +108,7 @@ var generateInteractionModel = function (skill) { }, ], }); - - let customSlotTypes = [ { name: 'EmailSlot', @@ -152,7 +163,7 @@ var generateInteractionModel = function (skill) { ], }, ]; - + let dialogPrompts = [ { id: 'Elicit.Intent-SendMessageIntent.IntentSlot-Name', @@ -190,7 +201,7 @@ var generateInteractionModel = function (skill) { ], }, ]; - + let dialogIntents = [ { name: 'SendMessageIntent', @@ -244,7 +255,7 @@ var generateInteractionModel = function (skill) { }; var uploadSkill = function (skill) { - let generatedInteractionModel = generateInteractionModel(skill); + let generatedInteractionModel = generateInteractionModel (skill); return fetch ( `https://api.amazonalexa.com/v0/skills/${skill.skillID}/interactionModel/locales/en-US`, { diff --git a/backend/models/alexa.js b/backend/models/alexa.js index 271b528..9723c1f 100644 --- a/backend/models/alexa.js +++ b/backend/models/alexa.js @@ -47,24 +47,31 @@ module.exports = { //Defaul Amazon handlers (some of them) handlers.NewSession = function () { - this.attributes['WantToHearQuestions'] = false; + this.attributes['WaitingQuestions'] = false; }; handlers['AMAZON.YesIntent'] = function () { console.log("yes intent"); - if (this.attributes['WantToHearQuestions']) { + if (this.attributes['WaitingQuestions']) { this.response .speak (listOfPossibleQuestions) - .listen(listOfPossibleQuestions); + .listen(constants.voiceResponseStrings.GENERIC_CONTINUE); this.emit(':responseReady'); } }; + handlers['AMAZON.NoIntent'] = function () { + console.log("No intent"); + if (this.attributes['WaitingQuestions']) { + this.attributes['WaitingQuestions'] = false; + } + }; + //Handler for launch request handlers = { LaunchRequest: function () { this.response - .speak (activeSkill.invocationAnswer) + .speak (activeSkill.invocationAnswer + ' Would you like to hear possible questions ?') .listen (constants.voiceResponseStrings.GENERIC_CONTINUE); //Phrase from listen doesn't work !!! this.attributes['WantToHearQuestions'] = true; From 2ae983d211a857d9a6c5dc7d371e696389173eef Mon Sep 17 00:00:00 2001 From: GotPPay Date: Tue, 23 Jan 2018 00:40:19 +0100 Subject: [PATCH 72/83] Experiment with Yes and No intent --- backend/helpers/amazon.js | 21 +++++++++++---------- backend/models/alexa.js | 6 +++--- 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/backend/helpers/amazon.js b/backend/helpers/amazon.js index f280ed4..13bf567 100644 --- a/backend/helpers/amazon.js +++ b/backend/helpers/amazon.js @@ -66,16 +66,17 @@ var generateInteractionModel = function (skill) { //Special Amazon intent - allIntents.push ( - { - name: 'AMAZON.YesIntent', - samples: [], - }, - { - name: 'AMAZON.NoIntent', - samples: [], - } - ); + allIntents.push ({ + name: 'YesIntent', + samples: ['Yes', 'Yes please', 'Sure'], + slots: [], + }); + + allIntents.push ({ + name: 'NoIntent', + samples: ['No', 'No thank you'], + slots: [], + }); skill.intents.map (intent => { allIntents.push ({name: intent.intentName, samples: intent.questions}); diff --git a/backend/models/alexa.js b/backend/models/alexa.js index 9723c1f..22cbdc7 100644 --- a/backend/models/alexa.js +++ b/backend/models/alexa.js @@ -50,7 +50,7 @@ module.exports = { this.attributes['WaitingQuestions'] = false; }; - handlers['AMAZON.YesIntent'] = function () { + handlers['YesIntent'] = function () { console.log("yes intent"); if (this.attributes['WaitingQuestions']) { this.response @@ -60,7 +60,7 @@ module.exports = { } }; - handlers['AMAZON.NoIntent'] = function () { + handlers['NoIntent'] = function () { console.log("No intent"); if (this.attributes['WaitingQuestions']) { this.attributes['WaitingQuestions'] = false; @@ -71,7 +71,7 @@ module.exports = { handlers = { LaunchRequest: function () { this.response - .speak (activeSkill.invocationAnswer + ' Would you like to hear possible questions ?') + .speak (activeSkill.invocationAnswer + ' Would you like to hear possible questions ?') .listen (constants.voiceResponseStrings.GENERIC_CONTINUE); //Phrase from listen doesn't work !!! this.attributes['WantToHearQuestions'] = true; From b2386ea0d6520796aef77c04e9dca5456333ecde Mon Sep 17 00:00:00 2001 From: GotPPay Date: Tue, 23 Jan 2018 01:03:59 +0100 Subject: [PATCH 73/83] Yes and No intent dont work with dialog --- backend/helpers/amazon.js | 14 -------------- backend/models/alexa.js | 31 ++++++------------------------- 2 files changed, 6 insertions(+), 39 deletions(-) diff --git a/backend/helpers/amazon.js b/backend/helpers/amazon.js index 13bf567..fcc781e 100644 --- a/backend/helpers/amazon.js +++ b/backend/helpers/amazon.js @@ -64,20 +64,6 @@ var generateInteractionModel = function (skill) { let result = {}; let allIntents = []; - //Special Amazon intent - - allIntents.push ({ - name: 'YesIntent', - samples: ['Yes', 'Yes please', 'Sure'], - slots: [], - }); - - allIntents.push ({ - name: 'NoIntent', - samples: ['No', 'No thank you'], - slots: [], - }); - skill.intents.map (intent => { allIntents.push ({name: intent.intentName, samples: intent.questions}); }); diff --git a/backend/models/alexa.js b/backend/models/alexa.js index 22cbdc7..a08f6fb 100644 --- a/backend/models/alexa.js +++ b/backend/models/alexa.js @@ -42,40 +42,21 @@ module.exports = { ''; } }); - console.log(listOfPossibleQuestions); //Defaul Amazon handlers (some of them) - handlers.NewSession = function () { - this.attributes['WaitingQuestions'] = false; - }; - - handlers['YesIntent'] = function () { - console.log("yes intent"); - if (this.attributes['WaitingQuestions']) { - this.response - .speak (listOfPossibleQuestions) - .listen(constants.voiceResponseStrings.GENERIC_CONTINUE); - this.emit(':responseReady'); - } - }; - - handlers['NoIntent'] = function () { - console.log("No intent"); - if (this.attributes['WaitingQuestions']) { - this.attributes['WaitingQuestions'] = false; - } - }; + handlers.NewSession = function () {}; //Handler for launch request handlers = { LaunchRequest: function () { this.response - .speak (activeSkill.invocationAnswer + ' Would you like to hear possible questions ?') + .speak ( + activeSkill.invocationAnswer + + '' + + listOfPossibleQuestions + ) .listen (constants.voiceResponseStrings.GENERIC_CONTINUE); //Phrase from listen doesn't work !!! - - this.attributes['WantToHearQuestions'] = true; - this.emit (':responseReady'); }, }; From 97b6755f2f9cccdcf3bdf6b4fc0320966acd8af2 Mon Sep 17 00:00:00 2001 From: GotPPay Date: Tue, 23 Jan 2018 01:40:19 +0100 Subject: [PATCH 74/83] No magic numbers --- backend/config/constants.js | 6 ++++++ backend/models/alexa.js | 4 ++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/backend/config/constants.js b/backend/config/constants.js index b60f24b..299010e 100644 --- a/backend/config/constants.js +++ b/backend/config/constants.js @@ -31,6 +31,12 @@ constants.voiceResponseStrings = { GENERIC_CONTINUE : 'Would you like to continue' } +//Timing is given in [ms] +constats.voiceResponseTimings = { + PAUSE_BETWEEN_QUESTIONS : 650, + PAUSE_AFTER_WELCOME_MESSAGE : 650, +} + module.exports = constants; \ No newline at end of file diff --git a/backend/models/alexa.js b/backend/models/alexa.js index a08f6fb..801c5f9 100644 --- a/backend/models/alexa.js +++ b/backend/models/alexa.js @@ -39,7 +39,7 @@ module.exports = { listOfPossibleQuestions += intent.intentExplanation + intent.questions[0] + - ''; + ''; } }); @@ -53,7 +53,7 @@ module.exports = { this.response .speak ( activeSkill.invocationAnswer + - '' + + '' + listOfPossibleQuestions ) .listen (constants.voiceResponseStrings.GENERIC_CONTINUE); //Phrase from listen doesn't work !!! From c6cd49a66f6281339f05d77490f071174f725f6c Mon Sep 17 00:00:00 2001 From: GotPPay Date: Tue, 23 Jan 2018 01:41:34 +0100 Subject: [PATCH 75/83] fix typo --- backend/config/constants.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/config/constants.js b/backend/config/constants.js index 299010e..7c3a7df 100644 --- a/backend/config/constants.js +++ b/backend/config/constants.js @@ -32,7 +32,7 @@ constants.voiceResponseStrings = { } //Timing is given in [ms] -constats.voiceResponseTimings = { +constants.voiceResponseTimings = { PAUSE_BETWEEN_QUESTIONS : 650, PAUSE_AFTER_WELCOME_MESSAGE : 650, } From 0d858ad1c7431349bec469cb5990a169bd21c10e Mon Sep 17 00:00:00 2001 From: GotPPay Date: Tue, 23 Jan 2018 01:46:02 +0100 Subject: [PATCH 76/83] List only questions with explanation --- backend/models/alexa.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/models/alexa.js b/backend/models/alexa.js index 801c5f9..3301a8e 100644 --- a/backend/models/alexa.js +++ b/backend/models/alexa.js @@ -35,7 +35,7 @@ module.exports = { let listOfPossibleQuestions = ''; activeSkill.intents.map (intent => { - if (intent.questions.length > 0) { + if (intent.questions.length > 0 && intent.intentExplanation) { listOfPossibleQuestions += intent.intentExplanation + intent.questions[0] + From 370edd6ef0c4f9a107241a34350e7e027944957a Mon Sep 17 00:00:00 2001 From: GotPPay Date: Tue, 23 Jan 2018 01:51:36 +0100 Subject: [PATCH 77/83] fix inherited explanation bug --- web/src/App.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/App.js b/web/src/App.js index 9f46a99..a91c5f6 100644 --- a/web/src/App.js +++ b/web/src/App.js @@ -255,7 +255,7 @@ class App extends Component { allIntents: this.state.allIntents, selectedIndex: NEW_INTENT_SELECTED_INDEX, launchRequest: false, - selectedIntent: {intentName: '', questions: [''], answer: ''}, + selectedIntent: {intentName: '', questions: [''], answer: '', intentExplanation:''}, }); } From 3202bf5f0ba2981cf22c9d8cbcb35513416b0091 Mon Sep 17 00:00:00 2001 From: GotPPay Date: Tue, 23 Jan 2018 01:56:07 +0100 Subject: [PATCH 78/83] Implement help intent --- backend/models/alexa.js | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/backend/models/alexa.js b/backend/models/alexa.js index 3301a8e..87d1b95 100644 --- a/backend/models/alexa.js +++ b/backend/models/alexa.js @@ -39,7 +39,9 @@ module.exports = { listOfPossibleQuestions += intent.intentExplanation + intent.questions[0] + - ''; + ''; } }); @@ -47,13 +49,22 @@ module.exports = { handlers.NewSession = function () {}; + handlers['AMAZON.HelpIntent'] = function () { + console.log ('Help intent'); + this.response + .speak (listOfPossibleQuestions) + .listen (constants.voiceResponseStrings.GENERIC_CONTINUE); //Phrase from listen doesn't work !!! + }; + //Handler for launch request handlers = { LaunchRequest: function () { this.response .speak ( activeSkill.invocationAnswer + - '' + + '' + listOfPossibleQuestions ) .listen (constants.voiceResponseStrings.GENERIC_CONTINUE); //Phrase from listen doesn't work !!! From 6b26db3d18b6ea6d376157f11db30abd61f4266b Mon Sep 17 00:00:00 2001 From: GotPPay Date: Tue, 23 Jan 2018 02:15:44 +0100 Subject: [PATCH 79/83] fix code --- backend/models/alexa.js | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/backend/models/alexa.js b/backend/models/alexa.js index 87d1b95..51ac02e 100644 --- a/backend/models/alexa.js +++ b/backend/models/alexa.js @@ -45,18 +45,7 @@ module.exports = { } }); - //Defaul Amazon handlers (some of them) - - handlers.NewSession = function () {}; - - handlers['AMAZON.HelpIntent'] = function () { - console.log ('Help intent'); - this.response - .speak (listOfPossibleQuestions) - .listen (constants.voiceResponseStrings.GENERIC_CONTINUE); //Phrase from listen doesn't work !!! - }; - - //Handler for launch request + //Handler for launch requestconsole.log() handlers = { LaunchRequest: function () { this.response @@ -170,6 +159,19 @@ module.exports = { } }; + //Defaul Amazon handlers (some of them) + + handlers.NewSession = function () { + console.log(this.event.request); + }; + + handlers['AMAZON.HelpIntent'] = function () { + console.log ('Help intent'); + this.response + .speak (listOfPossibleQuestions) + .listen (constants.voiceResponseStrings.GENERIC_CONTINUE); //Phrase from listen doesn't work !!! + }; + //Default handlers for unknown questions and session close handlers.Unhandled = function () { From 0a8eb2e280146f3cda62932e77ac6494437f4a6e Mon Sep 17 00:00:00 2001 From: GotPPay Date: Tue, 23 Jan 2018 02:18:08 +0100 Subject: [PATCH 80/83] fix code --- backend/models/alexa.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/backend/models/alexa.js b/backend/models/alexa.js index 51ac02e..63d9f16 100644 --- a/backend/models/alexa.js +++ b/backend/models/alexa.js @@ -161,10 +161,6 @@ module.exports = { //Defaul Amazon handlers (some of them) - handlers.NewSession = function () { - console.log(this.event.request); - }; - handlers['AMAZON.HelpIntent'] = function () { console.log ('Help intent'); this.response From cc579133c0688076636b38b8328097611b1f4bce Mon Sep 17 00:00:00 2001 From: GotPPay Date: Tue, 23 Jan 2018 02:23:59 +0100 Subject: [PATCH 81/83] Amazon default intents not working with dialog --- backend/models/alexa.js | 9 --------- 1 file changed, 9 deletions(-) diff --git a/backend/models/alexa.js b/backend/models/alexa.js index 63d9f16..61b390c 100644 --- a/backend/models/alexa.js +++ b/backend/models/alexa.js @@ -159,15 +159,6 @@ module.exports = { } }; - //Defaul Amazon handlers (some of them) - - handlers['AMAZON.HelpIntent'] = function () { - console.log ('Help intent'); - this.response - .speak (listOfPossibleQuestions) - .listen (constants.voiceResponseStrings.GENERIC_CONTINUE); //Phrase from listen doesn't work !!! - }; - //Default handlers for unknown questions and session close handlers.Unhandled = function () { From dc2c8f384e6ed1a9fb5a0a9af65aa3e27fab8536 Mon Sep 17 00:00:00 2001 From: GotPPay Date: Tue, 23 Jan 2018 12:15:04 +0100 Subject: [PATCH 82/83] use foreach instead of map --- backend/models/alexa.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/backend/models/alexa.js b/backend/models/alexa.js index 61b390c..c9a32e2 100644 --- a/backend/models/alexa.js +++ b/backend/models/alexa.js @@ -34,7 +34,7 @@ module.exports = { destinationEmail = activeSkill.contactEmail; let listOfPossibleQuestions = ''; - activeSkill.intents.map (intent => { + activeSkill.intents.forEach(intent => { if (intent.questions.length > 0 && intent.intentExplanation) { listOfPossibleQuestions += intent.intentExplanation + @@ -45,6 +45,8 @@ module.exports = { } }); + console.log(listOfPossibleQuestions); + //Handler for launch requestconsole.log() handlers = { LaunchRequest: function () { From d8b2f5f0b4e3efb36a771d569b6d944e84c737c7 Mon Sep 17 00:00:00 2001 From: GotPPay Date: Tue, 23 Jan 2018 12:18:20 +0100 Subject: [PATCH 83/83] fix typo --- backend/helpers/email.js | 2 +- backend/models/alexa.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/helpers/email.js b/backend/helpers/email.js index 76e109a..f13a0ce 100644 --- a/backend/helpers/email.js +++ b/backend/helpers/email.js @@ -26,7 +26,7 @@ module.exports = { return validEmailRegex.test (email); }, - sendEmal: function (name, fromEmail, message, toEmail) { + sendEmail: function (name, fromEmail, message, toEmail) { return new Promise ((resolve, reject) => { fromEmail = this.transformEmailFromAlexaResponse(fromEmail); let messageBody = diff --git a/backend/models/alexa.js b/backend/models/alexa.js index 4074784..309e210 100644 --- a/backend/models/alexa.js +++ b/backend/models/alexa.js @@ -121,7 +121,7 @@ module.exports = { console.log ('Email : ' + intent.slots.Email.value); console.log ('Message : ' + intent.slots.Message.value); emailHelper - .sendEmal ( + .sendEmail ( intent.slots.Name.value, intent.slots.Email.value, intent.slots.Message.value,