From f36c5d6fc97582f647b09f6fdc007953e3f7bcff Mon Sep 17 00:00:00 2001 From: Vanco Stojkov Date: Wed, 12 Jul 2017 01:19:15 +0200 Subject: [PATCH] Initial commit --- .env.example | 4 + .gitignore | 4 + README.md | 9 ++ index.js | 13 ++ package.json | 21 +++ server/express.js | 13 ++ server/index.js | 4 + server/intents/date.js | 93 +++++++++++++ server/intents/index.js | 12 ++ server/websocket.js | 56 ++++++++ yarn.lock | 288 ++++++++++++++++++++++++++++++++++++++++ 11 files changed, 517 insertions(+) create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 README.md create mode 100644 index.js create mode 100644 package.json create mode 100644 server/express.js create mode 100644 server/index.js create mode 100644 server/intents/date.js create mode 100644 server/intents/index.js create mode 100644 server/websocket.js create mode 100644 yarn.lock diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..22c143d --- /dev/null +++ b/.env.example @@ -0,0 +1,4 @@ +NODE_ENV=development +PORT=4040 +WSURL=http://localhost:4040 +APIAI_CLIENT_TOKEN=YOUR_API_AI_TOKEN diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..fbdaad6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +/client +/node_modules + +*.env diff --git a/README.md b/README.md new file mode 100644 index 0000000..0ff25a2 --- /dev/null +++ b/README.md @@ -0,0 +1,9 @@ +# Date Bot + +A simple Date bot that answers date related questions like: +- how many days between today and November 21st +- Is it Monday today +- How many days until December +- etc. + +Also API.AI's small talk agent included, which enables the bot to answers questions, well... related to small talk. diff --git a/index.js b/index.js new file mode 100644 index 0000000..06d9a1a --- /dev/null +++ b/index.js @@ -0,0 +1,13 @@ +const dotenv = require('dotenv').config({ + path: process.env.NODE_ENV === 'test' ? 'test.env' : + (process.env.NODE_ENV === 'production' ? 'production.env' : '.env') +}); +const server = require('./server'); +const app = require('./server/express'); +const socket = require('./server/websocket'); + +server.on('request', app); + +server.listen(process.env.PORT || 3000, () => { + console.info(`server/ws started on port ${process.env.PORT}`); // eslint-disable-line no-console +}); diff --git a/package.json b/package.json new file mode 100644 index 0000000..498f286 --- /dev/null +++ b/package.json @@ -0,0 +1,21 @@ +{ + "name": "ws-chat-bot", + "version": "0.1.0", + "description": "Rxjs Websockets", + "main": "index.js", + "scripts": { + "start": "node index.js", + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "Vanco Stojkov ", + "license": "ISC", + "dependencies": { + "apiai": "^4.0.2", + "dotenv": "^4.0.0", + "express": "^4.15.3", + "moment": "^2.18.1", + "rxjs": "^5.4.2", + "uuid": "^3.1.0", + "ws": "^3.0.0" + } +} diff --git a/server/express.js b/server/express.js new file mode 100644 index 0000000..5b24f9d --- /dev/null +++ b/server/express.js @@ -0,0 +1,13 @@ +const express = require('express'); + +const app = express(); +const router = express.Router(); + +router.use('/*', function(req, res){ + return res.sendFile('index.html', { + root: 'client/dist/' + }); +}); +app.use(router); + +module.exports = app; diff --git a/server/index.js b/server/index.js new file mode 100644 index 0000000..7e1ff9f --- /dev/null +++ b/server/index.js @@ -0,0 +1,4 @@ +const http = require('http'); +const server = http.createServer(); + +module.exports = server; diff --git a/server/intents/date.js b/server/intents/date.js new file mode 100644 index 0000000..aa2e489 --- /dev/null +++ b/server/intents/date.js @@ -0,0 +1,93 @@ +const moment = require('moment'); + +const daysOfWeekByDay = { + 'Sunday': 0, + 'Monday': 1, + 'Tuesday': 2, + 'Wednesday': 3, + 'Thursday': 4, + 'Friday': 5, + 'Saturday': 6 +}; + +const daysOfWeekByIndex = Object.keys(daysOfWeekByDay) + .reduce((acc, d, ix) => { + acc[ix] = d; + return acc; + }, {}); + +const plural = (num, unit) => num + ' ' + unit + (+num !== 1 ? 's' : ''); + +module.exports = { + "date.between": (pars) => { + const date1 = moment(pars['date-1']); + const date2 = moment(pars['date-2']); + const unit = pars.unit; + const diff = Math.abs(date1.diff(date2, unit)); + + return `There are ${plural(diff, unit)} between ` + date1.format('MMMM Do YYYY') + ' and ' + date2.format('MMMM Do YYYY'); + }, + "date.check": (pars) => { + const { location } = pars; + const date = moment(pars.date); + const now = moment(); + + return now.format("MM-DD-YYYY") === date.format("MM-DD-YYYY") + ? 'Yes' : 'No'; + }, + "date.day_of_week": (pars) => { + const { date, location } = pars; + + return daysOfWeekByIndex[moment(date).format('e')]; + }, + "date.day_of_week.check": (pars) => { + const { dayofweek, location } = pars; + const date = moment(pars.date); + const realDayOfWeek = date.format('e'); + + return +daysOfWeekByDay[dayofweek] === +realDayOfWeek + ? `Yes, it's ${daysOfWeekByIndex[realDayOfWeek]}` + : `No, it's ${daysOfWeekByIndex[realDayOfWeek]}`; + }, + "date.get": (pars) => { + const date = pars.date ? moment(date) : moment(); + + return date.format('MMMM Do YYYY'); + }, + "date.month.check": (pars) => { + const [start, end] = pars['date-period'].split("/"); + const now = moment(); + const month = +moment(start).format('M'); + + return +now.format('M') === month + ? 'Yes, it\'s ' + now.format('MMMM') + : 'No, it\'s ' + now.format('MMMM'); + }, + "date.month.get": () =>'It\'s ' + moment().format('MMMM'), + "date.since": (pars) => { + const { unit } = pars; + const date = moment(pars.date); + const now = moment(); + const diff = Math.abs(now.diff(date, unit)); + + return `${plural(diff, unit)} since ` + date.format('MMMM Do YYYY'); + }, + "date.until": (pars) => { + const now = moment(); + const date = moment(pars['date']); + const unit = pars.unit; + const diff = Math.abs(now.diff(date, unit)); + + return `${plural(diff, unit)} until ${date.format('MMMM Do YYYY')}`; + }, + "date.year.check": (pars) => { + const [start, end] = pars['date-period'].split("/"); + const now = moment(); + const year = +moment(start).format('YYYY'); + + return +now.format('YYYY') === year + ? 'Yes, it\'s ' + now.format('YYYY') + : 'No, it\'s ' + now.format('YYYY'); + }, + "date.year.get": () => 'The current year is ' + moment().format('YYYY'), +}; diff --git a/server/intents/index.js b/server/intents/index.js new file mode 100644 index 0000000..91c885a --- /dev/null +++ b/server/intents/index.js @@ -0,0 +1,12 @@ +const date = require('./date'); + +// merge all intent actions +const intents = Object.assign({}, date); + +function doIntent(action, pars) { + const unrecognizedActionMsg = 'I don\t understand that. Can you try again?'; + + return intents[action] ? intents[action](pars) : unrecognizedActionMsg; +} + +module.exports = doIntent; diff --git a/server/websocket.js b/server/websocket.js new file mode 100644 index 0000000..22b1d4f --- /dev/null +++ b/server/websocket.js @@ -0,0 +1,56 @@ +const server = require('./'); +const doIntent = require('./intents'); +const WebSocket = require('ws'); +const apiai = require("apiai")(process.env.APIAI_CLIENT_TOKEN); +const uuidv4 = require('uuid/v4'); +const util = require('util') + +const wss = new WebSocket.Server({server: server}); + +wss.on('connection', (ws) => { + ws.on('message', (msg) => { + try { + const input = JSON.parse(msg); + + if (input.type === 'user' && input.msg) { + const sessionId = input.sessionId || uuidv4(); + + callApiAi(input.msg, sessionId) + .then((response) => { + const { parameters, action, fulfillment } = response.result; + const output = doIntent(action, parameters); + + return ws.send(JSON.stringify({type: 'bot', msg: output})); + }) + .catch(error => + ws.send(JSON.stringify({type: 'bot', msg: 'Didn\'t quite get that?'})) + ); + } + } catch (err) { + // TODO: handle JSON.parse error + ws.send(JSON.stringify({type: 'bot', msg: 'Didn\'t quite get that?'})) + } + }); + + // TODO: generate ws session ids w/ uuid or sth. + ws.send(JSON.stringify({type: 'sessionId', msg: uuidv4()})); + // ws.send(JSON.stringify({type: 'bot', msg: 'Hi there!'})); + ws.send(JSON.stringify({type: 'bot', msg: '⏰ I\'m a date bot — ask me something about date!'})); +}); + +function callApiAi(text, sessionId) { + return new Promise((resolve, reject) => { + try { + // TODO: generate ws session ids w/ uuid or sth. + const request = apiai.textRequest(text, { sessionId: sessionId }); + + request.on('response', response => resolve(response)); + request.on('error', error => reject(error)); + request.end(); + } catch (err) { + reject(err); + } + }); +} + +module.exports = wss; diff --git a/yarn.lock b/yarn.lock new file mode 100644 index 0000000..375de2d --- /dev/null +++ b/yarn.lock @@ -0,0 +1,288 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +accepts@~1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.3.tgz#c3ca7434938648c3e0d9c1e328dd68b622c284ca" + dependencies: + mime-types "~2.1.11" + negotiator "0.6.1" + +apiai@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/apiai/-/apiai-4.0.2.tgz#9a4d6921877e43cee4719eb73609da1d520ac857" + +array-flatten@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" + +content-disposition@0.5.2: + version "0.5.2" + resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.2.tgz#0cf68bb9ddf5f2be7961c3a85178cb85dba78cb4" + +content-type@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.2.tgz#b7d113aee7a8dd27bd21133c4dc2529df1721eed" + +cookie-signature@1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" + +cookie@0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.3.1.tgz#e7e0a1f9ef43b4c8ba925c5c5a96e806d16873bb" + +debug@2.6.7: + version "2.6.7" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.7.tgz#92bad1f6d05bbb6bba22cca88bcd0ec894c2861e" + dependencies: + ms "2.0.0" + +depd@1.1.0, depd@~1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.0.tgz#e1bd82c6aab6ced965b97b88b17ed3e528ca18c3" + +destroy@~1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80" + +dotenv@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-4.0.0.tgz#864ef1379aced55ce6f95debecdce179f7a0cd1d" + +ee-first@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" + +encodeurl@~1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.1.tgz#79e3d58655346909fe6f0f45a5de68103b294d20" + +escape-html@~1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" + +etag@~1.8.0: + version "1.8.0" + resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.0.tgz#6f631aef336d6c46362b51764044ce216be3c051" + +express@^4.15.3: + version "4.15.3" + resolved "https://registry.yarnpkg.com/express/-/express-4.15.3.tgz#bab65d0f03aa80c358408972fc700f916944b662" + dependencies: + accepts "~1.3.3" + array-flatten "1.1.1" + content-disposition "0.5.2" + content-type "~1.0.2" + cookie "0.3.1" + cookie-signature "1.0.6" + debug "2.6.7" + depd "~1.1.0" + encodeurl "~1.0.1" + escape-html "~1.0.3" + etag "~1.8.0" + finalhandler "~1.0.3" + fresh "0.5.0" + merge-descriptors "1.0.1" + methods "~1.1.2" + on-finished "~2.3.0" + parseurl "~1.3.1" + path-to-regexp "0.1.7" + proxy-addr "~1.1.4" + qs "6.4.0" + range-parser "~1.2.0" + send "0.15.3" + serve-static "1.12.3" + setprototypeof "1.0.3" + statuses "~1.3.1" + type-is "~1.6.15" + utils-merge "1.0.0" + vary "~1.1.1" + +finalhandler@~1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.0.3.tgz#ef47e77950e999780e86022a560e3217e0d0cc89" + dependencies: + debug "2.6.7" + encodeurl "~1.0.1" + escape-html "~1.0.3" + on-finished "~2.3.0" + parseurl "~1.3.1" + statuses "~1.3.1" + unpipe "~1.0.0" + +forwarded@~0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.0.tgz#19ef9874c4ae1c297bcf078fde63a09b66a84363" + +fresh@0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.0.tgz#f474ca5e6a9246d6fd8e0953cfa9b9c805afa78e" + +http-errors@~1.6.1: + version "1.6.1" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.6.1.tgz#5f8b8ed98aca545656bf572997387f904a722257" + dependencies: + depd "1.1.0" + inherits "2.0.3" + setprototypeof "1.0.3" + statuses ">= 1.3.1 < 2" + +inherits@2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" + +ipaddr.js@1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.3.0.tgz#1e03a52fdad83a8bbb2b25cbf4998b4cffcd3dec" + +media-typer@0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" + +merge-descriptors@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" + +methods@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" + +mime-db@~1.27.0: + version "1.27.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.27.0.tgz#820f572296bbd20ec25ed55e5b5de869e5436eb1" + +mime-types@~2.1.11, mime-types@~2.1.15: + version "2.1.15" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.15.tgz#a4ebf5064094569237b8cf70046776d09fc92aed" + dependencies: + mime-db "~1.27.0" + +mime@1.3.4: + version "1.3.4" + resolved "https://registry.yarnpkg.com/mime/-/mime-1.3.4.tgz#115f9e3b6b3daf2959983cb38f149a2d40eb5d53" + +moment@^2.18.1: + version "2.18.1" + resolved "https://registry.yarnpkg.com/moment/-/moment-2.18.1.tgz#c36193dd3ce1c2eed2adb7c802dbbc77a81b1c0f" + +ms@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" + +negotiator@0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.1.tgz#2b327184e8992101177b28563fb5e7102acd0ca9" + +on-finished@~2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" + dependencies: + ee-first "1.1.1" + +parseurl@~1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.1.tgz#c8ab8c9223ba34888aa64a297b28853bec18da56" + +path-to-regexp@0.1.7: + version "0.1.7" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" + +proxy-addr@~1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-1.1.4.tgz#27e545f6960a44a627d9b44467e35c1b6b4ce2f3" + dependencies: + forwarded "~0.1.0" + ipaddr.js "1.3.0" + +qs@6.4.0: + version "6.4.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.4.0.tgz#13e26d28ad6b0ffaa91312cd3bf708ed351e7233" + +range-parser@~1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.0.tgz#f49be6b487894ddc40dcc94a322f611092e00d5e" + +rxjs@^5.4.2: + version "5.4.2" + resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-5.4.2.tgz#2a3236fcbf03df57bae06fd6972fd99e5c08fcf7" + dependencies: + symbol-observable "^1.0.1" + +safe-buffer@~5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.0.1.tgz#d263ca54696cd8a306b5ca6551e92de57918fbe7" + +send@0.15.3: + version "0.15.3" + resolved "https://registry.yarnpkg.com/send/-/send-0.15.3.tgz#5013f9f99023df50d1bd9892c19e3defd1d53309" + dependencies: + debug "2.6.7" + depd "~1.1.0" + destroy "~1.0.4" + encodeurl "~1.0.1" + escape-html "~1.0.3" + etag "~1.8.0" + fresh "0.5.0" + http-errors "~1.6.1" + mime "1.3.4" + ms "2.0.0" + on-finished "~2.3.0" + range-parser "~1.2.0" + statuses "~1.3.1" + +serve-static@1.12.3: + version "1.12.3" + resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.12.3.tgz#9f4ba19e2f3030c547f8af99107838ec38d5b1e2" + dependencies: + encodeurl "~1.0.1" + escape-html "~1.0.3" + parseurl "~1.3.1" + send "0.15.3" + +setprototypeof@1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.0.3.tgz#66567e37043eeb4f04d91bd658c0cbefb55b8e04" + +"statuses@>= 1.3.1 < 2", statuses@~1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.3.1.tgz#faf51b9eb74aaef3b3acf4ad5f61abf24cb7b93e" + +symbol-observable@^1.0.1: + version "1.0.4" + resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.0.4.tgz#29bf615d4aa7121bdd898b22d4b3f9bc4e2aa03d" + +type-is@~1.6.15: + version "1.6.15" + resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.15.tgz#cab10fb4909e441c82842eafe1ad646c81804410" + dependencies: + media-typer "0.3.0" + mime-types "~2.1.15" + +ultron@~1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/ultron/-/ultron-1.1.0.tgz#b07a2e6a541a815fc6a34ccd4533baec307ca864" + +unpipe@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" + +utils-merge@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.0.tgz#0294fb922bb9375153541c4f7096231f287c8af8" + +uuid@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.1.0.tgz#3dd3d3e790abc24d7b0d3a034ffababe28ebbc04" + +vary@~1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.1.tgz#67535ebb694c1d52257457984665323f587e8d37" + +ws@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/ws/-/ws-3.0.0.tgz#98ddb00056c8390cb751e7788788497f99103b6c" + dependencies: + safe-buffer "~5.0.1" + ultron "~1.1.0"