forked from van-ibm/watsonworkspace-bot
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit aab3182
Showing
9 changed files
with
363 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
node_modules | ||
.DS_Store | ||
.env |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
node_modules/ | ||
public/ | ||
.settings | ||
.project | ||
manifest.yml | ||
*.log | ||
.npmrc | ||
.env |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,66 @@ | ||
# IBM Watson Workspace Bot Framework | ||
|
||
This project is a framework for chatbot development on Watson Workspace. It is written in Javascript and [Node.js](https://nodejs.org). | ||
|
||
Developers can contribute chatbot behavior by simply listening to and respond to specific Workspace events. | ||
|
||
```javascript | ||
bot.webhooks.on('message-focus:ActionRequest:Schedule', (body, annotation) => { | ||
logger.info(`Checking calendars based on scheduling event phrase '${annotation.phrase}'`) | ||
}) | ||
``` | ||
|
||
Chatbot setup and event listening (webhooks) are handled by the bot framework. Developers choose which events and at what level to listen. In the code above, a developer could listen for the `message-focus` or `message-focus:ActionRequest` or `message-focus:ActionRequest:Schedule` event. For more information on the available events, see the Annotations, Focus, and Action Fulfillment [documentation](https://workspace.ibm.com/developer/docs). | ||
|
||
To build your bot, create a separate project. Then add the necessary require statements and begin listening to events to add your own behavior. | ||
|
||
```javascript | ||
const bot = require('watsonworkspace-bot') | ||
const ww = require('watsonworkspace-sdk') | ||
|
||
bot.webhooks.on('message-annotation-added', (message, annotation) => { | ||
// do something awesome using watsonworkspace-sdk | ||
}) | ||
|
||
bot.start() | ||
``` | ||
|
||
## Local Development | ||
### nodemon | ||
|
||
[Nodemon](https://github.com/remy/nodemon) is used for development. As you make changes to Javascript code, nodemon will automatically reload the bot with the latest changes. The ngrok tunnel is loaded separately. You do not need to restart the tunnel. Simply make changes to your source code and allow nodemon to reload the chatbot automatically. | ||
|
||
### dotenv | ||
[.Env](https://www.npmjs.com/package/dotenv) is used to store environment variables used by the bot: application IDs, secrets, etc. When doing local development, create a .env file in your project's folder with the following: | ||
|
||
``` | ||
NODE_ENV=development | ||
APP_ID=<your appId> | ||
APP_SECRET=<your appSecret> | ||
WEBHOOK_SECRET=<your webhookSecret> | ||
BOT_NAME=<your botName> | ||
``` | ||
|
||
Later when using Bluemix or similar PaaS solutions, you can edit the runtime variables to create the same property-value pairs. | ||
|
||
### ngrok | ||
|
||
Watson Workspace uses webhooks as an event-driven means to exchange information with your chatbot. This requires your chatbot to be listening on a public server. Rather than writing code and deploying to a public server during development, this starter uses [ngrok](https://ngrok.com/) automatically. | ||
|
||
Simply execute the `npm run-script dev` command. This will programmatically create a connection to a public domain using ngrok. A message will appear that indicates the URL you should use in your webhook. | ||
|
||
``` | ||
Use 'https://cdf9d82f.ngrok.io' as your webhook URL in Watson Workspace | ||
``` | ||
|
||
### winston | ||
|
||
[Winston](https://github.com/winstonjs/winston) is the preferred logger. | ||
|
||
## Production Deployment | ||
|
||
When moving your chatbot into production, you will need to edit your Webhook URL. | ||
|
||
1. Re-visit the [Listen to events](https://workspace.ibm.com/developer/apps/dashboard/webhooks) page. | ||
2. Select the more icon (three vertical dots) and then `Edit`. | ||
3. Update your Webhook URL to the productions server. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,136 @@ | ||
const botName = process.env.BOT_NAME || 'workspace-bot' | ||
const EventEmitter = require('events').EventEmitter | ||
const logger = require('winston') | ||
const webhooks = require('./webhooks') | ||
const ww = require('watsonworkspace-sdk') | ||
|
||
module.exports = new EventEmitter() | ||
module.exports.webhooks = webhooks | ||
|
||
if (process.env.NODE_ENV !== 'production') { | ||
require('dotenv').config() | ||
logger.level = 'verbose' | ||
} | ||
|
||
// watson work configuration use Bluemix user vars or edit .env file | ||
// these are provided when you register your appliction | ||
var webhookSecret = process.env.WEBHOOK_SECRET | ||
var appId = process.env.APP_ID | ||
var appSecret = process.env.APP_SECRET | ||
|
||
// dependencies | ||
var express = require('express') | ||
var http = require('http') | ||
var crypto = require('crypto') | ||
var bodyParser = require('body-parser') | ||
var methodOverride = require('method-override') | ||
|
||
// set up express | ||
var app = express() | ||
|
||
// all environments | ||
app.set('port', process.env.PORT || 3000) | ||
app.use(bodyParser.urlencoded({ | ||
extended: false | ||
})) | ||
app.use(bodyParser.json({limit: '5mb'})) | ||
app.use(methodOverride()) | ||
|
||
// watson work services middleware | ||
app.use(verifier) | ||
app.use(ignorer) | ||
app.use(webhook) | ||
|
||
module.exports.start = () => { | ||
http.createServer(app).listen(app.get('port'), '0.0.0.0', () => { | ||
logger.info(botName + ' bot listening on ' + app.get('port')) | ||
ww.authenticate(appId, appSecret, (token) => { | ||
module.exports.emit('authenticated', token) | ||
}) | ||
}) | ||
} | ||
|
||
/** | ||
* Middleware function to handle the Watson Work challenge | ||
*/ | ||
function verifier (req, res, next) { | ||
if (req.body.type === 'verification') { | ||
logger.verbose('Received webhook verification challenge ' + req.body.challenge) | ||
|
||
var bodyToSend = { | ||
response: req.body.challenge | ||
} | ||
|
||
var hashToSend = crypto.createHmac('sha256', webhookSecret) | ||
.update(JSON.stringify(bodyToSend)) | ||
.digest('hex') | ||
|
||
res.set('X-OUTBOUND-TOKEN', hashToSend) | ||
res.send(bodyToSend) | ||
} else { | ||
next() | ||
} | ||
} | ||
|
||
/** | ||
* Middleware function to ignore messages from this bot | ||
*/ | ||
function ignorer (req, res, next) { | ||
// Ignore our own messages | ||
if (req.body.userId === appId) { | ||
res.status(201).send().end() | ||
} else { | ||
// console.log('Sending body to next middleware ' + JSON.stringify(req.body)) | ||
next() | ||
} | ||
} | ||
|
||
var marks = [] | ||
|
||
function mark (messageId) { | ||
logger.verbose(`marking ${messageId} [${marks.length}]`) | ||
marks.push(messageId) | ||
} | ||
|
||
/** | ||
* Checks if a message ID has been read/sent previously from this application | ||
*/ | ||
function marked (messageId) { | ||
var p = false | ||
for (var i in marks) { | ||
if (marks[i] === messageId) { | ||
p = true | ||
break | ||
} | ||
} | ||
|
||
if (marks.length > 200) { | ||
marks = [] // housekeeping clear the array | ||
} | ||
|
||
// console.log(`${messageId} mark=${p}`) | ||
|
||
return p | ||
} | ||
|
||
/** | ||
* Middleware function to handle the webhook event | ||
*/ | ||
function webhook (req, res, next) { | ||
const body = req.body | ||
|
||
if (body.type) { | ||
logger.verbose(`Webhook event '${body.type}' for messageId ${body.messageId} with body`) | ||
logger.debug(body) | ||
|
||
// only handle messages that this bot has not seen before | ||
// if (!marked(body.messageId)) { | ||
webhooks.emitWebhook(body) | ||
// } | ||
} | ||
|
||
// you can acknowledge here or later | ||
// but you MUST respond or watson work will keep sending the message | ||
res.status(200).send().end() | ||
next() | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
require('dotenv').config() | ||
|
||
const ngrok = require('ngrok') | ||
|
||
ngrok.connect({ | ||
proto: 'http', | ||
addr: process.env.PORT, | ||
region: 'us' | ||
}, (err, url) => { | ||
if (err) { | ||
console.log(`Error creating ngrok ${err}`) | ||
} else { | ||
console.log(`Use '${url}' as your webhook URL in Watson Workspace`) | ||
} | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
{ | ||
"name": "watsonworkspace-bot", | ||
"version": "0.0.1", | ||
"description": "An unofficial IBM Watson Workspace bot kit", | ||
"scripts": { | ||
"start": "node index.js", | ||
"test": "jasmine", | ||
"dev": "concurrently \"node ngrok.js\" \"nodemon ./index.js\"" | ||
}, | ||
"keywords": [ | ||
"watsonworkspace", | ||
"ibm", | ||
"workspace" | ||
], | ||
"repository" : { | ||
"type" : "git", | ||
"url" : "https://github.com/van-ibm/watsonworkspace-bot.git" | ||
}, | ||
"author": "[email protected]", | ||
"license": "Apache-2.0", | ||
"dependencies": { | ||
"body-parser": "1.14.x", | ||
"dotenv": "^4.0.0", | ||
"express": "4.13.x", | ||
"jsonwebtoken": "^7.1.9", | ||
"method-override": "^2.3.6", | ||
"qs": "^6.3.0", | ||
"request": "^2.76.0", | ||
"winston": "^2.4.0" | ||
}, | ||
"repository": {}, | ||
"engines": { | ||
"node": "4.x" | ||
}, | ||
"devDependencies": { | ||
"concurrently": "^3.5.0", | ||
"jasmine": "^2.8.0", | ||
"ngrok": "^2.2.23", | ||
"nodemon": "^1.12.1" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
describe('workspace-bot', function () { | ||
// set a much longer timeout to allow interaction with Workspace UI | ||
// or long running webhook events | ||
jasmine.DEFAULT_TIMEOUT_INTERVAL = 300000 | ||
|
||
const spaceId = '57cf270ee4b06c8b753629e6' | ||
|
||
const bot = require('../index') | ||
const webhooks = bot.webhooks | ||
const ww = require('watsonworkspace-sdk') | ||
|
||
|
||
// open the localtunnel to allow webhook tests | ||
require('../localtunnel') | ||
|
||
it('start', function (done) { | ||
// listen for the token event to signal the app is ready to use | ||
bot.on('authenticated', (token) => { | ||
done() | ||
}) | ||
|
||
// start the app to begin setting up the server and webhooks | ||
bot.start() | ||
}) | ||
|
||
it('webhook-message-created', function (done) { | ||
ww.sendMessage(spaceId, 'Type any message into Workspace') | ||
|
||
webhooks.on('message-created', message => { | ||
expect(message).not.toBe(null) | ||
done() | ||
}) | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
{ | ||
"spec_dir": "spec", | ||
"spec_files": [ | ||
"**/*[sS]pec.js" | ||
], | ||
"helpers": [ | ||
"helpers/**/*.js" | ||
], | ||
"stopSpecOnExpectationFailure": false, | ||
"random": false | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
'use strict' | ||
|
||
const EventEmitter = require('events').EventEmitter | ||
const logger = require('winston') | ||
|
||
module.exports = new EventEmitter() | ||
|
||
module.exports.emitWebhook = (message) => { | ||
const type = message.type | ||
const annotationType = message.annotationType | ||
let annotationPayload = {} | ||
|
||
// the annotationPayload is a string that must be parsed to an object | ||
if (message.annotationPayload) { | ||
annotationPayload = JSON.parse(message.annotationPayload) | ||
|
||
// since we now have the payload, remove it from the message | ||
// and send it as a param in the emit event | ||
delete message.annotationPayload | ||
} | ||
|
||
logger.verbose(`Emiting '${type}' with message`) | ||
logger.debug(message) | ||
logger.verbose(`Emiting '${type}' with payload`) | ||
logger.debug(annotationPayload) | ||
|
||
// call the node event emitters | ||
// message-created or message-annotation-removed | ||
module.exports.emit(type, message, annotationPayload) | ||
|
||
// more granular annotation related events | ||
// 'message-focus' or 'actionSelected' | ||
module.exports.emit(annotationType, message, annotationPayload) | ||
|
||
// 'message-focus:ActionRequest' or 'message-focus:Question' | ||
if (annotationPayload.lens) { | ||
module.exports.emit(`${annotationType}:${annotationPayload.lens}`, message, annotationPayload) | ||
} | ||
|
||
// 'message-focus:ActionRequest:Schedule' | ||
if (annotationPayload.category) { | ||
module.exports.emit(`${annotationType}:${annotationPayload.lens}:${annotationPayload.category}`, message, annotationPayload) | ||
} | ||
|
||
// 'actionSelected:sample_button' | ||
if (annotationPayload.actionId) { | ||
module.exports.emit(`${annotationType}:${annotationPayload.actionId}`, message, annotationPayload) | ||
} | ||
} |