diff --git a/README.md b/README.md index ec7ab2c..fc36472 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", "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" + + 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/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/config/config.js b/backend/config/config.js new file mode 100644 index 0000000..67573de --- /dev/null +++ b/backend/config/config.js @@ -0,0 +1,17 @@ +var config = {}; + +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'; +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.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'; + +module.exports = config; diff --git a/backend/config/constants.js b/backend/config/constants.js new file mode 100644 index 0000000..7c3a7df --- /dev/null +++ b/backend/config/constants.js @@ -0,0 +1,42 @@ +const constants = {}; + +constants.amazonResultCodes = { + OK:200, + ACCEPTED:202, + BAD_REQUEST:400, + UNAUTHORIZED:401, + NOT_FOUND:404, + CONFLICT:409, + PAYLOAD_TOO_LARGE:413 +} + +constants.apiResultCodes = { + 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, + INCONSISTENT_STATE:5, +} + +constants.HTTPResultCodes = { + INTERNAL_SERVER_ERROR : 500, +} + +constants.SKILL_ID_LENGTH = 24; + +constants.voiceResponseStrings = { + QUESTION_NOT_FOUND : 'Sorry, I didnt understand', + GENERIC_CONTINUE : 'Would you like to continue' +} + +//Timing is given in [ms] +constants.voiceResponseTimings = { + PAUSE_BETWEEN_QUESTIONS : 650, + PAUSE_AFTER_WELCOME_MESSAGE : 650, +} + + + +module.exports = constants; \ No newline at end of file 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/controllers/index.js b/backend/controllers/index.js new file mode 100644 index 0000000..bdcb063 --- /dev/null +++ b/backend/controllers/index.js @@ -0,0 +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..e698d84 --- /dev/null +++ b/backend/controllers/saburlyEntryPoint.js @@ -0,0 +1,9 @@ +var express = require ('express'), router = express.Router (); +var bodyParser = require ('body-parser'); +var alexa = require ('../models/alexa'); + +router.post ('/', bodyParser.json (), async (req, res) => { + alexa.run (req, res); +}); + +module.exports = router; diff --git a/backend/controllers/skill.js b/backend/controllers/skill.js new file mode 100644 index 0000000..a0c2f5f --- /dev/null +++ b/backend/controllers/skill.js @@ -0,0 +1,130 @@ +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 ('../models/alexa'); + +router.get ('/:id', async (req, res, next) => { + const id = req.params.id; + + if (id.length !== constants.SKILL_ID_LENGTH) { + res.json ([]); + } else { + databaseHelper + .getSkill (id) + .then (result => { + res.json (result); + }) + .catch (err => { + res.json ([]); + }); + } +}); + +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); + let updateOnAmazon = skill.updateOnAmazon; + + delete skill.updateOnAmazon; + delete skill._id; + + //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) { + //We need to 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.updateModel (); + } else { + //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 => { + //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 { + //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, 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: ''}); + }); +}); + +module.exports = router; diff --git a/backend/express.js b/backend/express.js deleted file mode 100644 index 3c2fe22..0000000 --- a/backend/express.js +++ /dev/null @@ -1,120 +0,0 @@ -var express = require('express'); -var alexa = require('alexa-app'); - -var bodyParser = require('body-parser'); - -const dbURL = 'mongodb://localhost:27017/tellall'; -const PORT = 5000; - - -var MongoClient = require ('mongodb').MongoClient; -var ObjectID = require ('mongodb').ObjectID; - -const router = express.Router (); - -router.get ('/intents', 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); - }); - - } catch (e) { - console.log ('error:', e); - next (e); - } -}); - -router.get ('/deleteIntent/:id', async (req, res, next) => { - try { - let id = req.params.id; - let result = db.collection('intent_list').remove({_id: ObjectID(id)},(err,result)=>{ - if (err) throw err; - res.json(result); - }); - } catch (e) { - console.log ('error:', e); - next (e); - } -}); - -router.post ('/updateIntent/: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); - }); - } catch (e) { - console.log ('error:', e); - next (e); - } -}); - - - - -var app = express(); - -// ALWAYS setup the alexa app and attach it to express before anything else. -var alexaApp = new alexa.app('step3'); - -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 normal -alexaApp.launch(function(request, response) { - response.say("You launched Saburly app!"); -}); - -alexaApp.request = (jsonRequest) => { - const alexaRequest = new alexa.request(jsonRequest); - if (alexaRequest.type() === "IntentRequest") { - const intent = db.collection('intent_list').findOne({ - name: alexaRequest.data.request.intent.name - }); - if (intent) { - const response = new alexa.response(alexaRequest.getSession()); - return response.say(intent.answer); - } - } -}; - -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 (dbURL).then (database => { - db = database; - db.collection ('intent_list'); - app.listen (PORT, () => - console.log ('Express server running on port ' + PORT) - ); -}); \ No newline at end of file diff --git a/backend/helpers/amazon.js b/backend/helpers/amazon.js new file mode 100644 index 0000000..fcc781e --- /dev/null +++ b/backend/helpers/amazon.js @@ -0,0 +1,282 @@ +require ('isomorphic-fetch'); +const config = require ('../config/config'); +var request = require ('request'); +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 (result => { + return result.text (); + }); +}; + +var refreshTokens = function () { + 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); + } 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); + } + } + }); + }); +}; + +var generateInteractionModel = function (skill) { + let result = {}; + let allIntents = []; + + skill.intents.map (intent => { + allIntents.push ({name: intent.intentName, samples: intent.questions}); + }); + + //Special intent for sending message (Dialog) + + allIntents.push ({ + 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: 'EmailSlot', + samples: ['My email is {Email}', '{Email}'], + }, + { + name: 'Message', + type: 'MessageSlot', + samples: ['{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', + }, + }, + ], + }, + ]; + + result.interactionModel = {}; + + result.interactionModel.languageModel = { + invocationName: skill.invocationName, + types: customSlotTypes, + intents: allIntents, + }; + + result.interactionModel.prompts = dialogPrompts; + result.interactionModel.dialog = {}; + result.interactionModel.dialog.intents = dialogIntents; + + return JSON.stringify (result); +}; + +var uploadSkill = function (skill) { + let generatedInteractionModel = generateInteractionModel (skill); + return fetch ( + `https://api.amazonalexa.com/v0/skills/${skill.skillID}/interactionModel/locales/en-US`, + { + method: 'POST', + headers: { + Authorization: config.TOKEN, + }, + body: generatedInteractionModel, + } + ); +}; + +module.exports = { + updateSkill: function (skill) { + return new Promise ((resolve, reject) => { + if (new Date () / 1000 > config.TOKEN_EXPIRES_IN) { + refreshTokens () + .then (() => { + uploadSkill (skill).then (response => { + resolve (response.status); + }); + }) + .catch (e => { + reject (e); + }); + } else { + uploadSkill (skill) + .then (response => { + resolve (response.status); + }) + .catch (e => { + reject (e); + }); + } + }); + }, +}; diff --git a/backend/helpers/database.js b/backend/helpers/database.js new file mode 100644 index 0000000..6934339 --- /dev/null +++ b/backend/helpers/database.js @@ -0,0 +1,99 @@ +const config = require ('../config/config'); +var ObjectID = require ('mongodb').ObjectID; + +var db = null; + +module.exports = { + initModule: function (databaseObject) { + db = databaseObject; + db.collection ('intent_list'); + }, + + loadTokens: function () { + db + .collection ('token_list') + .findOne () + .then (tokens => { + if (tokens !== null) { + config.TOKEN = tokens.access_token; + config.REFRESH_TOKEN = tokens.refresh_token; + config.TOKEN_EXPIRES_IN = tokens.expires_in; + } else { + //Cannot continue without tokens + console.log ('Cannot continue without tokens in database'); + process.exit (-1); + } + }) + .catch (e => { + console.log ( + 'Error loading tokens ! Cannot continue without tokens in database' + ); + process.exit (-1); + }); + }, + + updateTokens: function (refresh_token, access_token, expires_in) { + return new Promise ((resolve, reject) => { + let newTokenDocument = { + id: 1, + refresh_token: refresh_token, + access_token: access_token, + expires_in: new Date () / 1000 + expires_in, + }; + db + .collection ('token_list') + .update ({id: 1}, newTokenDocument, {upsert: true}, (err, result) => { + if (err) { + reject (err) + }else{ + config.REFRESH_TOKEN = refresh_token; + config.TOKEN = access_token; + config.TOKEN_EXPIRES_IN = newTokenDocument.expires_in; + resolve (); + } + }); + }); + }, + + getSkill: function (skillDbID) { + return new Promise ((resolve, reject) => { + db + .collection ('skill_list') + .findOne ({_id: ObjectID (skillDbID)}, (err, skill) => { + if (skill) { + resolve (skill); + } else { + reject (err); + } + }); + }); + }, + + updateSkill: function (id, skill) { + return new Promise ((resolve, reject) => { + db + .collection ('skill_list') + .update ({_id: ObjectID (id)}, skill, {upsert: true}, (err, result) => { + if (err){ + reject(); + }else{ + resolve(); + } + }); + }); + }, + + deleteSkill: function (id) { + return new Promise ((resolve, reject) => { + db + .collection ('skill_list') + .remove ({_id: ObjectID (id)}, (err, result) => { + if (err){ + reject (err); + }else{ + resolve (result); + } + }); + }); + } +}; diff --git a/backend/helpers/email.js b/backend/helpers/email.js new file mode 100644 index 0000000..f13a0ce --- /dev/null +++ b/backend/helpers/email.js @@ -0,0 +1,75 @@ +const nodemailer = require ('nodemailer'); +const emailConfig = require('../config/email'); + +module.exports = { + transformEmailFromAlexaResponse: 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 (/\s/g, '') //remove all spaces + .replace (/at/gi, '@') + .replace (/underscore/gi, '_') + .replace (/dash/gi, '-') + .replace (/dot/gi, '.'); + + return transformedEmail; + }, + + 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); + }, + + sendEmail: 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 + + '\r\nName : ' + + name + + '\r\nEmail : ' + + fromEmail + + '\r\nYour Saburly team'; + + let messageBodyHTML = + '
Hello. User left you a message on Saburly service using Alexa skill.
' + + message + + '