107 Commits

Author SHA1 Message Date
GotPPay
9b58f7745b improve code 2018-01-25 15:56:45 +01:00
GotPPay
b0737efb4e add missing spread operator 2018-01-25 15:39:48 +01:00
GotPPay
5d2a15bc93 apply comment from PR 2018-01-25 15:19:30 +01:00
GotPPay
145fff3b51 fix alexa bug with no speak action 2018-01-24 13:14:47 +01:00
GotPPay
4f6c714fa3 Rephrase continuation question 2018-01-24 13:11:35 +01:00
GotPPay
f714fdf70a improve No intent 2018-01-24 13:07:08 +01:00
GotPPay
adc1f1c099 improve default built-in intents 2018-01-24 13:02:46 +01:00
GotPPay
c0a177d39b experiment with yes no help cancel 2018-01-23 14:40:01 +01:00
GotPPay
3700d9bb58 Simulate built-in intents 2018-01-23 13:46:02 +01:00
GotPPay
7a5ddc6b52 Experiment with user-defined help intent 2018-01-23 13:33:53 +01:00
GotPPay
39b8e7608e Test built-in intents 2018-01-23 13:24:56 +01:00
MirnaM
a8ffe0a5c0 Merge pull request #11 from GotPPay/send-message-feature
Send message feature
2018-01-23 12:19:57 +01:00
MirnaM
def591224e Merge pull request #12 from GotPPay/question-explanation
Question explanation
2018-01-23 12:19:14 +01:00
GotPPay
d8b2f5f0b4 fix typo 2018-01-23 12:18:20 +01:00
GotPPay
dc2c8f384e use foreach instead of map 2018-01-23 12:15:04 +01:00
GotPPay
cc579133c0 Amazon default intents not working with dialog 2018-01-23 02:23:59 +01:00
GotPPay
0a8eb2e280 fix code 2018-01-23 02:18:08 +01:00
GotPPay
6b26db3d18 fix code 2018-01-23 02:15:44 +01:00
GotPPay
3202bf5f0b Implement help intent 2018-01-23 01:56:07 +01:00
GotPPay
370edd6ef0 fix inherited explanation bug 2018-01-23 01:51:36 +01:00
GotPPay
0d858ad1c7 List only questions with explanation 2018-01-23 01:46:02 +01:00
GotPPay
c6cd49a66f fix typo 2018-01-23 01:41:34 +01:00
GotPPay
97b6755f2f No magic numbers 2018-01-23 01:40:19 +01:00
GotPPay
b2386ea0d6 Yes and No intent dont work with dialog 2018-01-23 01:03:59 +01:00
GotPPay
2ae983d211 Experiment with Yes and No intent 2018-01-23 00:40:19 +01:00
GotPPay
b1a853c363 experiment with Yes and No intents 2018-01-23 00:20:50 +01:00
GotPPay
d58d4b89e3 experiment with yes intent 2018-01-23 00:08:39 +01:00
GotPPay
2c6953fe97 list all questions on Launch 2018-01-22 23:35:25 +01:00
GotPPay
af19108e9c fix InteractionModel generator 2018-01-22 22:34:58 +01:00
GotPPay
a00859c594 fix InteractionModel generator 2018-01-22 22:34:13 +01:00
GotPPay
085a0324b3 . 2018-01-22 20:12:16 +01:00
GotPPay
d8799fa40d fix JSON model 2018-01-19 23:26:59 +01:00
GotPPay
de0a4f3176 fix reverting skill on failed update 2018-01-19 22:54:50 +01:00
GotPPay
48578d3ffe Change UI 2018-01-19 22:42:37 +01:00
GotPPay
e2d648980b fix code 2018-01-19 20:28:26 +01:00
GotPPay
b73086291c code fix and improvements 2018-01-19 20:23:50 +01:00
GotPPay
434d45248c fix error in validation and undefined email value 2018-01-19 20:15:27 +01:00
GotPPay
00272ec67d validate email using regex 2018-01-19 20:05:49 +01:00
GotPPay
0727328e58 improve email preview 2018-01-19 19:53:49 +01:00
GotPPay
e2d76ff03e improve email preview 2018-01-19 19:50:21 +01:00
GotPPay
9cff3cd9ae change email service 2018-01-19 19:28:46 +01:00
GotPPay
2f82709f11 improve dialog ; send email 2018-01-19 16:29:47 +01:00
MirnaM
f6bf3a05b2 Merge pull request #10 from GotPPay/switch-to-alexa-sdk
Switch to alexa sdk
2018-01-19 11:24:23 +01:00
GotPPay
4b594898b1 Complete dialogtest 2018-01-19 08:00:51 +01:00
GotPPay
b07a9e21b3 Testing dialog handler 2018-01-19 07:43:10 +01:00
GotPPay
a570640fe1 hande unknown questions 2018-01-18 22:07:23 +01:00
GotPPay
9e4b06bd4c keep session open - fix 2018-01-18 21:56:56 +01:00
GotPPay
6bfd4adcaf keep session open 2018-01-18 21:50:26 +01:00
GotPPay
48badce0f0 fix 'res not defined' 2018-01-18 21:43:25 +01:00
GotPPay
68287d49ff complete switch to new alexa package 2018-01-18 21:33:44 +01:00
GotPPay
2f59e12aa7 test new library 2018-01-18 19:50:13 +01:00
MirnaM
3e37c93395 Merge pull request #7 from GotPPay/inconsistent-state-fix
Inconsistent state fix
2018-01-16 17:05:21 +01:00
GotPPay
cd041c0131 code improvements 2018-01-16 16:56:56 +01:00
MirnaM
0607f1e98d Merge pull request #9 from GotPPay/UI-text-fix
Ui text fix
2018-01-16 14:48:51 +01:00
GotPPay
5db270d957 fix missing css on build 2018-01-16 13:48:39 +01:00
GotPPay
389a71b05a UI fix 2018-01-16 13:38:05 +01:00
GotPPay
e5783740cf fix token expired state 2018-01-16 01:44:33 +01:00
GotPPay
1a5f4586a8 inconsistent state fix 2018-01-16 00:40:03 +01:00
GotPPay
a78189f1a1 removed doubled function ; return object instead of array with one element 2018-01-16 00:08:38 +01:00
GotPPay
cd938c3981 additional edit for api refactoring 2018-01-15 23:44:50 +01:00
MirnaM
2bce02ab4e Merge pull request #6 from GotPPay/api-refactoring
Api refactoring
2018-01-15 16:34:33 +01:00
GotPPay
6859f567a9 frontend api refactoring 2018-01-15 16:27:12 +01:00
GotPPay
6e7bccea86 frontend api refactoring 2018-01-15 16:09:41 +01:00
GotPPay
7e4b959ffa backend api refactoring 2018-01-15 16:09:09 +01:00
GotPPay
e1f315cb81 backend api refactoring 2018-01-15 16:06:37 +01:00
MirnaM
49d30f00f8 Merge pull request #5 from GotPPay/step-4
Step 4
2018-01-15 14:56:03 +01:00
GotPPay
1b04e72b98 Revert changes 2018-01-15 14:52:34 +01:00
GotPPay
07857fd2f4 frontend code refactoring 2018-01-14 01:00:35 +01:00
GotPPay
7f56a28509 backend code refactoring 2018-01-13 14:57:41 +01:00
GotPPay
ff7e564d2e conflict fix 2018-01-12 08:23:34 +01:00
GotPPay
bbec4e9940 . 2018-01-12 02:12:05 +01:00
GotPPay
b80843cb97 ... 2018-01-12 01:56:17 +01:00
Bilal
4c8c1c5e0e change utterances to correct form;change intents to mimic dialog; 2018-01-11 14:05:44 +00:00
GotPPay
7d79c03d15 initial step 4 for online test 2018-01-11 04:24:16 +01:00
GotPPay
3214a2bea4 fixed delete bug, code refactoring, improved UI 2018-01-10 13:27:09 +01:00
Bilal
84c11f1b2f Keep session open 2018-01-08 21:13:23 +00:00
GotPPay
1a9227e76d refactoring 2018-01-08 21:50:24 +01:00
GotPPay
cd48599f39 Update readme 2018-01-08 13:53:50 +01:00
GotPPay
61bd6862aa removed unnecessary function 2018-01-08 13:49:24 +01:00
Bilal
994670ce54 from server ; working with amazon test service, not working with real device 2018-01-08 12:44:38 +00:00
Bilal
51ff7f255c from server ; intents working - launch request not working 2018-01-08 12:06:25 +00:00
Bilal
8a59d452f5 from server 2018-01-08 10:33:15 +00:00
Bilal
7fbc105433 .. 2018-01-07 17:51:07 +00:00
GotPPay
44ef80b7dd . 2018-01-07 17:53:46 +01:00
Bilal
675861d2cd almost there 2018-01-07 16:39:21 +00:00
Bilal
9ba9209aa3 working frontend ; backend not working 2018-01-07 13:54:15 +00:00
Bilal
64ff4357d1 Merge branch 'bilal-step-3-token-refresh' of https://github.com/GotPPay/tellall into bilal-step-3-token-refresh 2018-01-05 23:00:00 +00:00
GotPPay
3fe9ebde9d new scss file 2018-01-05 23:59:26 +01:00
Bilal
f1e4010950 from server 2018-01-05 22:28:55 +00:00
GotPPay
b7a778691f install ejs 2018-01-05 23:06:37 +01:00
GotPPay
beb575ef7f remove view engine 2018-01-05 22:53:53 +01:00
Bilal
db5bd107a0 add view engine 2018-01-05 19:40:05 +00:00
Bilal
214e6dff71 Merge branch 'bilal-step-3-token-refresh' of https://github.com/GotPPay/tellall into bilal-step-3-token-refresh 2018-01-05 19:33:48 +00:00
GotPPay
c3bba7ffa7 express routes reorder 2018-01-05 20:32:55 +01:00
Bilal
1f05d227b2 Changes from server 2018-01-05 19:20:30 +00:00
GotPPay
2990fad6cf package update 2018-01-05 20:15:56 +01:00
GotPPay
03c346f5d2 popup info ; token update 2018-01-05 17:56:24 +01:00
GotPPay
b55dd56859 readme update 2018-01-05 01:00:10 +01:00
GotPPay
32e0f4d6d7 automatic token refresh, initial stage 2018-01-05 00:51:49 +01:00
GotPPay
9652645339 fix 2017-12-04 12:18:05 +01:00
GotPPay
124885b41d clean backend 2017-12-03 18:47:24 +01:00
GotPPay
e8ee77a40d remove unused items 2017-12-03 18:25:10 +01:00
GotPPay
94d2883c01 send update to amazon 2017-12-03 17:38:58 +01:00
GotPPay
74ee9de93f generate interaction model 2017-12-03 00:15:11 +01:00
GotPPay
d5120a1ba2 functional application without amazon update 2017-12-02 22:48:45 +01:00
GotPPay
0e193fa5a8 new design 2017-12-01 11:03:48 +01:00
GotPPay
4f36fc7738 stage 3 2017-11-30 17:43:24 +01:00
50 changed files with 16629 additions and 256 deletions

98
README.md Normal file
View File

@@ -0,0 +1,98 @@
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 products client ID. To access this information, navigate to Amazons Developer Console. After youve 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 products client secret. To access this information, navigate to Amazons Developer Console. After youve 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 apps 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 Amazons Developer Console. After youve 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 websites client secret. To access this information, navigate to Amazons Developer Console. After youve 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

17
backend/config/config.js Normal file
View File

@@ -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;

View File

@@ -0,0 +1,43 @@
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 : 'Say something to continue',
DIDNT_ASK_ANYTHING : 'There was no question to answer to',
}
//Timing is given in [ms]
constants.voiceResponseTimings = {
PAUSE_BETWEEN_QUESTIONS : 650,
PAUSE_AFTER_WELCOME_MESSAGE : 650,
}
module.exports = constants;

14
backend/config/email.js Normal file
View File

@@ -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;

View File

@@ -0,0 +1,6 @@
var express = require ('express'), router = express.Router ();
router.use ('/skill', require ('./skill'));
router.use ('/saburly', require('./saburlyEntryPoint'));
module.exports = router;

View File

@@ -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;

View File

@@ -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;

310
backend/helpers/amazon.js Normal file
View File

@@ -0,0 +1,310 @@
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});
});
//Built-In like intents (Amazon built-in don't work, probably something related to existance of dialog intent
allIntents.push ({
name: 'HelpIntent',
samples: ['Help', 'Can you help me', 'I need help'],
slots: [],
},
{
name: 'CancelIntent',
samples: ['Cancel', 'Stop', 'Please stop'],
slots: [],
},
{
name: 'YesIntent',
samples: [
'Yes',
'Yes please',
'I would like that',
'Yes I would like that',
],
slots: [],
},
{
name: 'NoIntent',
samples: ['No', 'No thank you'],
slots: [],
});
//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);
});
}
});
},
};

View File

@@ -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);
}
});
});
}
};

75
backend/helpers/email.js Normal file
View File

@@ -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 =
'<p>Hello. User left you a message on Saburly service using Alexa skill.</p><br/><b>Message : </b><br/><p>' +
message +
'</p><br/><b>Name : </b>' +
name +
'<br/><b>Email : </b>' +
fromEmail +
'<br/><br/><b>Your Saburly team</b>';
let transporter = nodemailer.createTransport ({
host: emailConfig.SMTP_HOST,
port: emailConfig.PORT,
secure: emailConfig.SECURE,
auth: emailConfig.AUTH,
});
var mailOptions = {
from: emailConfig.FROM_EMAIL,
replyTo: fromEmail,
to: toEmail,
subject: emailConfig.SUBJECT,
text: messageBody,
html: messageBodyHTML,
};
transporter.sendMail (mailOptions, (error, info) => {
if (error) {
reject (error);
} else {
resolve (info);
}
});
});
},
};

View File

@@ -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, PUT');
res.header ('Access-Control-Allow-Credentials', 'true');
next ();
};

231
backend/models/alexa.js Normal file
View File

@@ -0,0 +1,231 @@
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) {
// Build the context manually, because Amazon Lambda is missing
var context = {
succeed: function (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);
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 = {};
destinationEmail = activeSkill.contactEmail;
let listOfPossibleQuestions = '';
activeSkill.intents.forEach (intent => {
if (intent.questions.length > 0 && intent.intentExplanation) {
listOfPossibleQuestions +=
intent.intentExplanation +
intent.questions[0] +
'<break time="' +
constants.voiceResponseTimings.PAUSE_BETWEEN_QUESTIONS +
'ms"/>';
}
});
listOfPossibleQuestions += 'If you dont know what to do, just say help or stop';
//Handler for launch requestconsole.log()
handlers = {
LaunchRequest: function () {
this.response
.speak (
activeSkill.invocationAnswer +
'<break time="' +
constants.voiceResponseTimings.PAUSE_AFTER_WELCOME_MESSAGE +
'ms"/>' +
'Would you like to hear list of questions that you can ask me'
)
.listen (constants.voiceResponseStrings.GENERIC_CONTINUE); //Phrase from listen doesn't work !!!
this.attributes['LaunchRequestYesNo'] = true;
this.emit (':responseReady');
},
};
//Handlers for user defined questions
activeSkill.intents.map (intent => {
handlers[intent.intentName] = function () {
if (this.attributes['LaunchRequestYesNo']){
this.attributes['LaunchRequestYesNo'] = false;
}
this.response
.speak (intent.answer)
.listen (constants.voiceResponseStrings.GENERIC_CONTINUE); //Phrase from listen doesn't work !!!
this.emit (':responseReady');
};
});
//Handler for sending message
handlers.SendMessageIntent = function () {
if (this.attributes['LaunchRequestYesNo']){
this.attributes['LaunchRequestYesNo'] = false;
}
let intent = this.event.request.intent;
console.log ('Dialog state : ' + this.event.request.dialogState);
console.log (intent);
//STARTED, IN_PROGRESS
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) {
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 =
'Sorry, that was not valid email. What is your email';
const repromptSpeech = speechOutput;
this.emit (
':elicitSlot',
slotToElicit,
speechOutput,
repromptSpeech
);
} else {
//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
.sendEmail (
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');
});
}
};
//Built-In intents
handlers.CancelIntent = function () {
if (this.attributes['LaunchRequestYesNo']){
this.attributes['LaunchRequestYesNo'] = false;
}
this.response.speak ('Thank you for using Saburly');
this.emit (':responseReady');
};
handlers.HelpIntent = function () {
if (this.attributes['LaunchRequestYesNo']){
this.attributes['LaunchRequestYesNo'] = false;
}
this.response
.speak (listOfPossibleQuestions)
.listen (constants.voiceResponseStrings.GENERIC_CONTINUE); //Phrase from listen doesn't work !!!
this.emit (':responseReady');
};
handlers.YesIntent = function(){
if (this.attributes['LaunchRequestYesNo']){
this.attributes['LaunchRequestYesNo'] = false;
this.emit('HelpIntent');
}else{
this.response.speak(constants.voiceResponseStrings.DIDNT_ASK_ANYTHING).listen(constants.voiceResponseStrings.GENERIC_CONTINUE);
this.emit(':responseReady');
}
}
handlers.NoIntent = function (){
if (this.attributes['LaunchRequestYesNo']){
this.attributes['LaunchRequestYesNo'] = false;
this.response.speak('').listen(constants.voiceResponseStrings.GENERIC_CONTINUE);
this.emit(':responseReady');
}else{
this.response.speak(constants.voiceResponseStrings.DIDNT_ASK_ANYTHING).listen(constants.voiceResponseStrings.GENERIC_CONTINUE);
this.emit(':responseReady');
}
}
//Default handler for unknown question
handlers.Unhandled = function () {
this.response
.speak (constants.voiceResponseStrings.QUESTION_NOT_FOUND)
.listen (constants.voiceResponseStrings.GENERIC_CONTINUE);
};
//Session handlers
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
console.log ('Error. Skill doesnt exist');
});
},
};

View File

@@ -211,6 +211,16 @@
"concat-map": "0.0.1"
}
},
"bson": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/bson/-/bson-1.0.4.tgz",
"integrity": "sha1-k8ENOeqltYQVy8QFLz5T5WKwtyw="
},
"buffer-shims": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/buffer-shims/-/buffer-shims-1.0.0.tgz",
"integrity": "sha1-mXjOMXOIxkmth5MCjDR37wRKi1E="
},
"builtin-modules": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz",
@@ -428,6 +438,19 @@
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.1.tgz",
"integrity": "sha1-eePVhlU0aQn+bw9Fpd5oEDspTSA="
},
"encoding": {
"version": "0.1.12",
"resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.12.tgz",
"integrity": "sha1-U4tm8+5izRq1HsMjgp0flIDHS+s=",
"requires": {
"iconv-lite": "0.4.19"
}
},
"es6-promise": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-3.2.1.tgz",
"integrity": "sha1-7FYjOGgDKQkgcXDDlEjiREndH8Q="
},
"escape-html": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
@@ -711,11 +734,30 @@
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.5.2.tgz",
"integrity": "sha1-1LUFvemUaYfM8PxY2QEP+WB+P6A="
},
"is-stream": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz",
"integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ="
},
"is-typedarray": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz",
"integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo="
},
"isarray": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
"integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE="
},
"isomorphic-fetch": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-2.2.1.tgz",
"integrity": "sha1-YRrhrPFPXoH3KVB0coGf6XM1WKk=",
"requires": {
"node-fetch": "1.7.3",
"whatwg-fetch": "2.0.3"
}
},
"isstream": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz",
@@ -819,6 +861,25 @@
"brace-expansion": "1.1.8"
}
},
"mongodb": {
"version": "2.2.33",
"resolved": "https://registry.npmjs.org/mongodb/-/mongodb-2.2.33.tgz",
"integrity": "sha1-tTfEcdNKZlG0jzb9vyl1A0Dgi1A=",
"requires": {
"es6-promise": "3.2.1",
"mongodb-core": "2.1.17",
"readable-stream": "2.2.7"
}
},
"mongodb-core": {
"version": "2.1.17",
"resolved": "https://registry.npmjs.org/mongodb-core/-/mongodb-core-2.1.17.tgz",
"integrity": "sha1-pBizN6FKFJkPtRC5I97mqBMXPfg=",
"requires": {
"bson": "1.0.4",
"require_optional": "1.0.1"
}
},
"ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
@@ -840,6 +901,15 @@
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz",
"integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk="
},
"node-fetch": {
"version": "1.7.3",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-1.7.3.tgz",
"integrity": "sha512-NhZ4CsKx7cYm2vSrBAr2PvFOe6sWDf0UYLRqA6svUYg7+/TSfVAu49jYC4BvQ4Sms9SZgdqGBgroqfDhJdTyKQ==",
"requires": {
"encoding": "0.1.12",
"is-stream": "1.1.0"
}
},
"node-forge": {
"version": "0.7.1",
"resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.7.1.tgz",
@@ -910,6 +980,11 @@
"resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz",
"integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns="
},
"process-nextick-args": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz",
"integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M="
},
"proxy-addr": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.2.tgz",
@@ -945,6 +1020,20 @@
"unpipe": "1.0.0"
}
},
"readable-stream": {
"version": "2.2.7",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.2.7.tgz",
"integrity": "sha1-BwV6y+JGeyIELTb5jFrVBwVOlbE=",
"requires": {
"buffer-shims": "1.0.0",
"core-util-is": "1.0.2",
"inherits": "2.0.3",
"isarray": "1.0.0",
"process-nextick-args": "1.0.7",
"string_decoder": "1.0.3",
"util-deprecate": "1.0.2"
}
},
"request": {
"version": "2.83.0",
"resolved": "https://registry.npmjs.org/request/-/request-2.83.0.tgz",
@@ -974,6 +1063,15 @@
"uuid": "3.1.0"
}
},
"require_optional": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/require_optional/-/require_optional-1.0.1.tgz",
"integrity": "sha512-qhM/y57enGWHAe3v/NcwML6a3/vfESLe/sGM2dII+gEO0BpKRUkWZow/tyloNqJyN6kXSl3RyyM8Ll5D/sJP8g==",
"requires": {
"resolve-from": "2.0.0",
"semver": "5.4.1"
}
},
"resolve": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.5.0.tgz",
@@ -983,6 +1081,11 @@
"path-parse": "1.0.5"
}
},
"resolve-from": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-2.0.0.tgz",
"integrity": "sha1-lICrIOlP+h2egKgEx+oUdhGWa1c="
},
"safe-buffer": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz",
@@ -991,8 +1094,7 @@
"semver": {
"version": "5.4.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.4.1.tgz",
"integrity": "sha512-WfG/X9+oATh81XtllIo/I8gOiY9EXRdv1cQdyykeXK17YcUW3EXUAi2To4pcH6nZtJPr7ZOpM5OMyWJZm+8Rsg==",
"optional": true
"integrity": "sha512-WfG/X9+oATh81XtllIo/I8gOiY9EXRdv1cQdyykeXK17YcUW3EXUAi2To4pcH6nZtJPr7ZOpM5OMyWJZm+8Rsg=="
},
"send": {
"version": "0.16.1",
@@ -1065,6 +1167,14 @@
"resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz",
"integrity": "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew=="
},
"string_decoder": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz",
"integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==",
"requires": {
"safe-buffer": "5.1.1"
}
},
"stringstream": {
"version": "0.0.5",
"resolved": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.5.tgz",
@@ -1182,6 +1292,11 @@
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
"integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw="
},
"util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8="
},
"utils-merge": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
@@ -1212,6 +1327,11 @@
"extsprintf": "1.3.0"
}
},
"whatwg-fetch": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-2.0.3.tgz",
"integrity": "sha1-nITsLc9oGH/wC8ZOEnS0QhduHIQ="
},
"wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",

View File

@@ -4,10 +4,14 @@
"description": "",
"main": "test.js",
"dependencies": {
"alexa-sdk": "^1.0.25",
"body-parser": "^1.13.1",
"ejs": "^2.3.1",
"ejs": "^2.5.7",
"express": "^4.13.0",
"alexa-app": "4.2.0"
"isomorphic-fetch": "^2.2.1",
"mongodb": "^2.2.33",
"nodemailer": "^4.4.1",
"request": "^2.83.0"
},
"author": "Matt Kruse <github@mattkruse.com> (http://mattkruse.com/)",
"license": "MIT"

28
backend/server.js Normal file
View File

@@ -0,0 +1,28 @@
var databaseHelper = require ('./helpers/database');
const config = require ('./config/config');
var express = require ('express');
var alexa = require ('./models/alexa');
var MongoClient = require ('mongodb').MongoClient;
var ObjectID = require ('mongodb').ObjectID;
const router = express.Router ();
var app = express ();
app.set ('view engine', 'ejs'); // Should be removed
app.use (require ('./middleware')); //common middleware for all requests
app.use (require ('./controllers')); //all routes
MongoClient.connect (config.DB_URL)
.then (database => {
databaseHelper.initModule (database);
app.listen (config.PORT, () => {
console.log ('Express server running on port ' + config.PORT);
alexa.updateModel ();
databaseHelper.loadTokens ();
});
})
.catch (e => {
console.log ('error : ' + e);
});

View File

@@ -1,43 +0,0 @@
var express = require("express");
var alexa = require("alexa-app");
var PORT = process.env.port || 5000;
var app = express();
// ALWAYS setup the alexa app and attach it to express before anything else.
var alexaApp = new alexa.app("step1");
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
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.listen(PORT);
console.log("Listening on port " + PORT + ", try http://localhost:" + PORT + "/step1");

View File

@@ -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();

View File

@@ -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;

79
test.js
View File

@@ -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));
});

23
web/.gitignore vendored Normal file
View File

@@ -0,0 +1,23 @@
# See https://help.github.com/ignore-files/ for more about ignoring files.
# dependencies
/node_modules
# testing
/coverage
# production
/build
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*
src/**/*.css

10539
web/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

30
web/package.json Normal file
View File

@@ -0,0 +1,30 @@
{
"name": "web",
"version": "0.1.0",
"private": true,
"dependencies": {
"keymaster": "^1.6.2",
"react": "^16.2.0",
"react-dom": "^16.2.0",
"react-md": "^1.2.8",
"react-popup": "^0.9.1",
"react-scripts": "1.0.17",
"webfontloader": "^1.6.28"
},
"scripts": {
"build-css": "node-sass-chokidar --include-path ./node_modules src/ -o src/",
"watch-css-mine": "npm run build-css && npm run build-css --watch --recursive",
"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",
"react-build" : "react-scripts build",
"build": "npm-run-all -p build-css react-build",
"test": "react-scripts test --env=jsdom",
"eject": "react-scripts eject"
},
"devDependencies": {
"node-sass": "^4.7.2",
"nodemon": "^1.12.1",
"npm-run-all": "^4.1.2"
}
}

BIN
web/public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

40
web/public/index.html Normal file
View File

@@ -0,0 +1,40 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="theme-color" content="#000000">
<!--
manifest.json provides metadata used when your web app is added to the
homescreen on Android. See https://developers.google.com/web/fundamentals/engage-and-retain/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json">
<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico">
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>React App</title>
</head>
<body>
<noscript>
You need to enable JavaScript to run this app.
</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>

15
web/public/manifest.json Normal file
View File

@@ -0,0 +1,15 @@
{
"short_name": "React App",
"name": "Create React App Sample",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
}
],
"start_url": "./index.html",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}

306
web/src/App.js Normal file
View File

@@ -0,0 +1,306 @@
import React, {Component} from 'react';
import './css/App.css';
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,
RESULT_CODES,
} from './config/constants';
class App extends Component {
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,
};
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);
}
render () {
let rightPanel;
switch (this.state.selectedIndex) {
case LAUNCH_REQUEST_SELECTED_INDEX:
rightPanel = (
<LaunchRequest
invocationName={this.state.invocationName}
invocationAnswer={this.state.invocationAnswer}
onSaveClick={this.handleSaveLaunchRequestClick}
waiting={this.state.waiting}
/>
);
break;
case CONTACT_SELECTED_INDEX:
rightPanel = (
<Contact
contactEmail={this.state.contactEmail}
onSaveEmailClick={this.handleSaveEmailClick}
waiting={this.state.waiting}
/>
);
break;
default:
rightPanel = (
<IntentDetails
selectedIntent={this.state.selectedIntent}
onDeleteIntentClick={this.handleDeleteIntentClick}
onSaveIntentClick={this.handleSaveIntentClick}
waiting={this.state.waiting}
/>
);
}
return (
<div className="App">
<Popup />
<div className="App-header">
<h1> Tell All </h1>
</div>
<IntentList
allIntents={this.state.allIntents}
onLaunchRequestClick={this.handleLaunchRequestClick}
onContactClick={this.handleContactClick}
onIntentClick={this.handleIntentClick}
onAddIntentClick={this.handleAddIntentClick}
selectedIndex={this.state.selectedIndex}
waiting={this.state.waiting}
/>
{rightPanel}
</div>
);
}
createSkill (intents, name, answer, email, updateOnAmazon) {
return {
_id: this.state._id,
skillID: this.state.skillID,
intents: intents,
invocationName: name,
invocationAnswer: answer,
contactEmail: email,
updateOnAmazon: updateOnAmazon,
};
}
handleIntentClick (selectedIntent, index) {
this.setState ({
selectedIntent: selectedIntent,
selectedIndex: index,
launchRequest: false,
});
}
handleLaunchRequestClick () {
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.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) {
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;
});
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);
}
}
}
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) {
//new intent
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,
};
}
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: '', intentExplanation:''},
});
}
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);
});
});
}
}
export default App;

8
web/src/App.test.js Normal file
View File

@@ -0,0 +1,8 @@
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
it('renders without crashing', () => {
const div = document.createElement('div');
ReactDOM.render(<App />, div);
});

View File

@@ -0,0 +1,48 @@
import React, { Component } from 'react';
import {Button, TextField} from 'react-md';
import '../css/Common.css';
import '../css/components/Contact.css';
import {EMAIL_MAX_LENGTH} from '../config/constants';
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 (
<div className="RightPanelBox">
<h5 className="PanelSubTitle"> Contact address will be used for direct messaging through Alexa </h5>
<TextField
id="contact email"
lineDirection="center"
label="Contact email"
className="md-cell md-cell--bottom ContactEmailInput"
maxLength={EMAIL_MAX_LENGTH}
onChange={this.handleEmailEdit}
value={this.state.contactEmail}/>
<br></br>
<br></br>
<br></br>
<Button className="SaveButton" flat primary swapTheming
onClick={()=>{this.props.onSaveEmailClick(this.state.contactEmail)}}
disabled={this.props.waiting}>Save</Button>
</div>
);
}
handleEmailEdit(e){
if (e.length === EMAIL_MAX_LENGTH) return;
this.setState({contactEmail: e});
}
}
export default Contact;

View File

@@ -0,0 +1,134 @@
import React, { Component } from 'react';
import {Button, SVGIcon, TextField} from 'react-md';
import '../css/components/IntentDetails.css';
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){
super(props);
this.state= {intent: props.selectedIntent};
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);
this.handleIntentExplanationEdit = this.handleIntentExplanationEdit.bind(this);
}
componentWillReceiveProps(props){
this.setState({intent: props.selectedIntent});
}
render() {
return (
<div className="RightPanelBox">
<div className="QuestionBox">
<h5 className="PanelSubTitle"> 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.</h5>
<TextField
id="intent explanation"
lineDirection="center"
placeholder="To ask us about our services, say "
className="md-cell md-cell--bottom IntentDetailsInputBoxes"
onChange={this.handleIntentExplanationEdit}
maxLength={INTENT_EXPLANATION_MAX_LENGTH}
value={this.state.intent.intentExplanation} />
<br/>
<TextField
id="intent name"
lineDirection="center"
label="Question name"
className="md-cell md-cell--bottom IntentDetailsInputBoxes"
onChange={this.handleIntentNameEdit}
maxLength={INTENT_NAME_MAX_LENGTH}
value={this.state.intent.intentName} />
</div>
<h5 className="QuestionTitle">Question variants</h5>
{
this.state.intent.questions.map((question, index)=>{
return (
<div key={index} className="QuestionBox">
<TextField
id="intent question"
lineDirection="center"
placeholder="Question"
className="md-cell md-cell--bottom IntentDetailsInputBoxes"
maxLength={QUESTION_MAX_LENGTH}
rightIcon={<SVGIcon onClick={()=>{this.deleteQuestion(question)}}> <path fill="#000000" d="M19,4H15.5L14.5,3H9.5L8.5,4H5V6H19M6,19A2,2 0 0,0 8,21H16A2,2 0 0,0 18,19V7H6V19Z"/> </SVGIcon>}
onChange={(e)=>{this.handleQuestionEdit(e,index)}}
value={question}/>
</div>
);
})
}
<br></br>
{
<div className="QuestionBox">
<TextField
id="intent answer"
lineDirection="center"
label="Answer"
placeholder="Answer"
maxLength={ANSWER_MAX_LENGTH}
className="md-cell md-cell--bottom IntentDetailsInputBoxes"
onChange={this.handleAnswerEdit}
value={this.state.intent.answer}/>
</div>
}
<br></br>
<br></br>
<Button className="IntentDetailsButton" flat primary onClick={()=>{this.props.onDeleteIntentClick(this.state.intent)}} disabled={this.props.waiting}>Delete question</Button>
<Button className="IntentDetailsButton" flat primary swapTheming onClick={this.addQuestion} disabled={this.props.waiting}>Add variant</Button>
<Button className="IntentDetailsButton" flat primary swapTheming onClick={()=>{this.props.onSaveIntentClick(this.state.intent)}} disabled={this.props.waiting}>Save</Button>
</div>
);
}
addQuestion(){
let newIntent = this.state.intent;
newIntent.questions.push('');
this.setState({intent: newIntent});
}
deleteQuestion(question){
let newIntent = this.state.intent;
let removeId = newIntent.questions.indexOf(question);
if (removeId !== -1)
newIntent.questions.splice(removeId,1);
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});
}
handleIntentExplanationEdit(e,index){
if (e.length === INTENT_EXPLANATION_MAX_LENGTH) return;
let newIntent = this.state.intent;
newIntent.intentExplanation = 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;
let newIntent = this.state.intent;
newIntent.intentName = e;
this.setState({intent: newIntent});
}
}
export default IntentDetails;

View File

@@ -0,0 +1,35 @@
import React, { Component } from 'react';
import {Button} from 'react-md';
import '../css/components/IntentItem.css'
import {INTENT_TITLE_MAX_LENGTH, INTENT_TITLE_TOOLTIP_DELAY} from '../config/constants'
class IntentItem extends Component {
constructor(props){
super(props);
this.state={intent: props.intent, index: props.index, onClick: props.onClick};
}
render() {
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 (
<div>
<Button className={this.props.selectedIndex===this.state.index ? 'IntentItem-selected' : 'IntentItem'}
onClick={()=>{this.state.onClick(this.state.intent,this.state.index)}}
flat
disabled={this.props.waiting}
tooltipDelay={INTENT_TITLE_TOOLTIP_DELAY}
tooltipLabel={this.state.intent.intentName.length>INTENT_TITLE_MAX_LENGTH ? this.state.intent.questions[0] : ''}>
{buttonTitle}
</Button>
<br></br>
</div>
);
}
}
export default IntentItem;

View File

@@ -0,0 +1,54 @@
import React, { Component } from 'react';
import {Button} from 'react-md';
import IntentItem from './IntentItem';
import '../css/components/IntentList.css';
import {
LAUNCH_REQUEST_SELECTED_INDEX,
CONTACT_SELECTED_INDEX} from '../config/constants'
class IntentList extends Component {
constructor (props){
super(props);
this.state = {intents: props.allIntents, selectedIndex:props.selectedIndex, onIntentClick:props.onIntentClick};
}
componentWillReceiveProps(props){
this.setState({intents: props.allIntents, selectedIndex: props.selectedIndex, onIntentClick: props.onIntentClick});
}
render() {
return (
<div className="IntentList">
<Button className={this.props.selectedIndex===LAUNCH_REQUEST_SELECTED_INDEX ? "LaunchRequestButton-selected" : "LaunchRequestButton"} flat primary
onClick={this.props.onLaunchRequestClick}
disabled={this.props.waiting} >Launch request</Button>
<Button className={this.props.selectedIndex===CONTACT_SELECTED_INDEX ? "ContactButton-selected" : "ContactButton"} flat primary
onClick={this.props.onContactClick}
disabled={this.props.waiting} >Contact</Button>
<div className="IntentList-title">
<h3>Questions</h3>
</div>
{
this.state.intents.map((intent,index)=>{
return <IntentItem
key={intent.intentName} intent={intent} index={index}
selectedIndex={this.props.selectedIndex}
onClick={this.state.onIntentClick}
waiting={this.props.waiting}>
</IntentItem>
})
}
<br></br>
<Button className="AddIntent" flat primary swapTheming
onClick={this.props.onAddIntentClick}
disabled={this.props.waiting}>Add question</Button>
</div>
);
}
}
export default IntentList;

View File

@@ -0,0 +1,65 @@
import React, { Component } from 'react';
import {Button, TextField} from 'react-md';
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 {
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 (
<div className="RightPanelBox">
<h5 className="PanelSubTitle"> Invocation name customers use to activate the skill. For example "Open Saburly" or "Talk to Saburly" </h5>
<TextField
id="invocation name"
lineDirection="center"
placeholder="Saburly"
label="Invocation name"
className="md-cell md-cell--bottom InvocationInputBoxes"
maxLength={INVOCATION_NAME_MAX_LENGTH}
onChange={this.handleNameEdit}
value={this.state.invocationName}/>
<br></br>
<h5 className="PanelSubTitle" >Answer customers get from Alexa when they activate the skill.</h5>
<TextField
id="invocation answer"
lineDirection="center"
placeholder="We are Saburly, ask us something about us"
label="Answer"
className="md-cell md-cell--bottom InvocationInputBoxes"
maxLength={INVOCATION_ANSWER_MAX_LENGTH}
onChange={this.handleAnswerEdit}
value={this.state.invocationAnswer}/>
<br></br>
<br></br>
<Button className="SaveButton" flat primary swapTheming
onClick={()=>{this.props.onSaveClick(this.state.invocationName, this.state.invocationAnswer)}}
disabled={this.props.waiting}>Save</Button>
</div>
);
}
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;

4
web/src/config/config.js Normal file
View File

@@ -0,0 +1,4 @@
//export const BASE_URL = 'tellall.saburly.com'; //for server
export const BASE_URL = 'localhost:5000'; //for local

View File

@@ -0,0 +1,21 @@
export const INTENT_NAME_MAX_LENGTH = 30;
export const INTENT_EXPLANATION_MAX_LENGTH = 70;
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;
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,
};

13
web/src/css/App.css Normal file
View File

@@ -0,0 +1,13 @@
.App {
text-align: center; }
.App-header {
background-color: white;
height: 80px;
padding: 20px; }
.App-title {
font-size: 1.5em; }
.App-intro {
font-size: large; }

17
web/src/css/App.scss Normal file
View File

@@ -0,0 +1,17 @@
.App {
text-align: center;
}
.App-header {
background-color:white;
height: 80px;
padding: 20px;
}
.App-title {
font-size: 1.5em;
}
.App-intro {
font-size: large;
}

21
web/src/css/Common.scss Normal file
View File

@@ -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;
}

View File

@@ -0,0 +1,5 @@
@import 'react-md/src/scss/react-md';
// Any variable overrides. The following just changes the default theme to use teal and purple.
$md-primary-color: $md-teal-500;
$md-secondary-color: $md-purple-a-400;

View File

@@ -0,0 +1,4 @@
.ContactEmailInput{
width: 60%;
margin-left: 20px;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -0,0 +1,11 @@
.ExplanationText{
float: left;
margin-top: 30px;
margin-left: 20px;
text-align: left;
}
.InvocationInputBoxes{
width: 60%;
margin-left: 20px;
}

3627
web/src/css/index.css Normal file

File diff suppressed because it is too large Load Diff

8
web/src/css/index.scss Normal file
View File

@@ -0,0 +1,8 @@
@import 'globals';
@include react-md-everything;
body {
margin: 0;
padding: 0;
}

174
web/src/css/popup.scss Normal file
View File

@@ -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,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") 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,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") 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; } }

16
web/src/index.js Normal file
View File

@@ -0,0 +1,16 @@
import React from 'react';
import ReactDOM from 'react-dom';
import './css/index.css';
import App from './App';
import registerServiceWorker from './registerServiceWorker';
import WebFontLoader from 'webfontloader';
WebFontLoader.load({
google: {
families: ['Roboto:300,400,500,700', 'Material Icons'],
},
});
ReactDOM.render(<App />, document.getElementById('root'));
registerServiceWorker();

20
web/src/lib/api.js Normal file
View File

@@ -0,0 +1,20 @@
import fetch from 'isomorphic-fetch';
import {BASE_URL} from '../config/config';
export const getSkill = (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}/skill/${id}`
return fetch(url, {
method: 'PUT',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify(skill),
});
}

View File

@@ -0,0 +1,108 @@
// In production, we register a service worker to serve assets from local cache.
// This lets the app load faster on subsequent visits in production, and gives
// it offline capabilities. However, it also means that developers (and users)
// will only see deployed updates on the "N+1" visit to a page, since previously
// cached resources are updated in the background.
// To learn more about the benefits of this model, read https://goo.gl/KwvDNy.
// This link also includes instructions on opting out of this behavior.
const isLocalhost = Boolean(
window.location.hostname === 'localhost' ||
// [::1] is the IPv6 localhost address.
window.location.hostname === '[::1]' ||
// 127.0.0.1/8 is considered localhost for IPv4.
window.location.hostname.match(
/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
)
);
export default function register() {
if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
// The URL constructor is available in all browsers that support SW.
const publicUrl = new URL(process.env.PUBLIC_URL, window.location);
if (publicUrl.origin !== window.location.origin) {
// Our service worker won't work if PUBLIC_URL is on a different origin
// from what our page is served on. This might happen if a CDN is used to
// serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374
return;
}
window.addEventListener('load', () => {
const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
if (isLocalhost) {
// This is running on localhost. Lets check if a service worker still exists or not.
checkValidServiceWorker(swUrl);
} else {
// Is not local host. Just register service worker
registerValidSW(swUrl);
}
});
}
}
function registerValidSW(swUrl) {
navigator.serviceWorker
.register(swUrl)
.then(registration => {
registration.onupdatefound = () => {
const installingWorker = registration.installing;
installingWorker.onstatechange = () => {
if (installingWorker.state === 'installed') {
if (navigator.serviceWorker.controller) {
// At this point, the old content will have been purged and
// the fresh content will have been added to the cache.
// It's the perfect time to display a "New content is
// available; please refresh." message in your web app.
console.log('New content is available; please refresh.');
} else {
// At this point, everything has been precached.
// It's the perfect time to display a
// "Content is cached for offline use." message.
console.log('Content is cached for offline use.');
}
}
};
};
})
.catch(error => {
console.error('Error during service worker registration:', error);
});
}
function checkValidServiceWorker(swUrl) {
// Check if the service worker can be found. If it can't reload the page.
fetch(swUrl)
.then(response => {
// Ensure service worker exists, and that we really are getting a JS file.
if (
response.status === 404 ||
response.headers.get('content-type').indexOf('javascript') === -1
) {
// No service worker found. Probably a different app. Reload the page.
navigator.serviceWorker.ready.then(registration => {
registration.unregister().then(() => {
window.location.reload();
});
});
} else {
// Service worker found. Proceed as normal.
registerValidSW(swUrl);
}
})
.catch(() => {
console.log(
'No internet connection found. App is running in offline mode.'
);
});
}
export function unregister() {
if ('serviceWorker' in navigator) {
navigator.serviceWorker.ready.then(registration => {
registration.unregister();
});
}
}