Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 17 additions & 4 deletions .babelrc
Original file line number Diff line number Diff line change
@@ -1,6 +1,19 @@
{
"presets": [
["env", {
"targets": {
"node": "0.12"
}
}]
],
"compact": false,
"presets": ["es2015"],
"auxiliaryCommentBefore": "istanbul ignore next",
"sourceMaps": "inline"
}
"sourceMaps": "inline",
"plugins": [
"transform-async-to-bluebird",
"transform-promise-to-bluebird",
["transform-runtime", {
"polyfill": false,
"regenerator": true
}]
]
}
3 changes: 2 additions & 1 deletion .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ globals:

parserOptions:
sourceType: "module"

ecmaVersion: 8

rules:
# ERRORS
space-before-blocks: 2
Expand Down
10 changes: 8 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,18 +37,24 @@
"cibuild": "npm run build"
},
"dependencies": {
"babel-preset-env": "^1.5.1",
"babel-preset-es2015": "^6.6.0",
"bluebird": "^3.5.0",
"body-parser": "^1.15.2",
"chai-as-promised": "^7.1.1",
"debug": "^2.2.0",
"express": "^4.14.0",
"jsonwebtoken": "^7.1.9",
"request": "^2.73.0"
},
"devDependencies": {
"babel-cli": "^6.10.1",
"chai": "^3.5.0",
"eslint": "^3.3.0",
"mocha": "^2.5.3"
"mocha": "^2.5.3",
"babel-cli": "^6.24.1",
"babel-plugin-transform-async-to-bluebird": "^1.1.1",
"babel-plugin-transform-promise-to-bluebird": "^1.1.1",
"babel-plugin-transform-runtime": "^6.23.0"
},
"engines": {
"node": ">=6.9.1",
Expand Down
194 changes: 97 additions & 97 deletions src/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,16 @@ import * as https from 'https';
import * as oauth from './oauth';
import * as ssl from './ssl';
import debug from 'debug';
import Promise from 'bluebird';

// Debug log
const log = debug('watsonwork-echo-app');

const post = Promise.promisify(request.post);

// Echoes Watson Work chat messages containing 'hello' or 'hey' back
// to the space they were sent to
export const echo = (appId, token) => (req, res) => {
export const echo = (appId, token) => async (req, res) => {
// Respond to the Webhook right away, as the response message will
// be sent asynchronously
res.status(201).end();
Expand All @@ -38,52 +41,58 @@ export const echo = (appId, token) => (req, res) => {
.filter((word) => /^(hello|hey)$/i.test(word)).length)

// Send the echo message
send(req.body.spaceId,
await send(req.body.spaceId,
util.format(
'Hey %s, did you say %s?',
req.body.userName, req.body.content),
token(),
(err, res) => {
if(!err)
log('Sent message to space %s', req.body.spaceId);
});
token());
log('Sent message to space %s', req.body.spaceId);
};

// Send an app message to the conversation in a space
const send = (spaceId, text, tok, cb) => {
request.post(
'https://api.watsonwork.ibm.com/v1/spaces/' + spaceId + '/messages', {
headers: {
Authorization: 'Bearer ' + tok
},
json: true,
// An App message can specify a color, a title, markdown text and
// an 'actor' useful to show where the message is coming from
body: {
type: 'appMessage',
version: 1.0,
annotations: [{
type: 'generic',
const send = async (spaceId, text, tok) => {
let res;
try {
res = await post(
'https://api.watsonwork.ibm.com/v1/spaces/' + spaceId + '/messages', {
headers: {
Authorization: 'Bearer ' + tok
},
json: true,
// An App message can specify a color, a title, markdown text and
// an 'actor' useful to show where the message is coming from
body: {
type: 'appMessage',
version: 1.0,

color: '#6CB7FB',
title: 'Echo message',
text: text,

actor: {
name: 'Sample echo app'
}
}]
}
}, (err, res) => {
if(err || res.statusCode !== 201) {
log('Error sending message %o', err || res.statusCode);
cb(err || new Error(res.statusCode));
return;
}
annotations: [{
type: 'generic',
version: 1.0,

color: '#6CB7FB',
title: 'Echo message',
text: text,

actor: {
name: 'Sample echo app'
}
}]
}
});

// Handle invalid status response code
if (res.statusCode !== 201)
throw new Error(res.statusCode);

// log the valid response and its body
else
log('Send result %d, %o', res.statusCode, res.body);
cb(null, res.body);
});
}
catch(err) {
// log the error and rethrow it
log('Error sending message %o', err);
throw err;
}
return res;
};

// Verify Watson Work request signature
Expand Down Expand Up @@ -113,73 +122,64 @@ export const challenge = (wsecret) => (req, res, next) => {
};

// Create Express Web app
export const webapp = (appId, secret, wsecret, cb) => {
export const webapp = async (appId, secret, wsecret) => {
// Authenticate the app and get an OAuth token
oauth.run(appId, secret, (err, token) => {
if(err) {
cb(err);
return;
}

// Return the Express Web app
cb(null, express()

// Configure Express route for the app Webhook
.post('/echo',
const token = await oauth.run(appId, secret);
// Configure Express route for the app Webhook
return express().post('/echo',

// Verify Watson Work request signature and parse request body
bparser.json({
type: '*/*',
verify: verify(wsecret)
}),
// Verify Watson Work request signature and parse request body
bparser.json({
type: '*/*',
verify: verify(wsecret)
}),

// Handle Watson Work Webhook challenge requests
challenge(wsecret),
// Handle Watson Work Webhook challenge requests
challenge(wsecret),

// Handle Watson Work messages
echo(appId, token)));
});
// Handle Watson Work messages
echo(appId, token));
};

// App main entry point
const main = (argv, env, cb) => {
// Create Express Web app
webapp(
env.ECHO_APP_ID, env.ECHO_APP_SECRET,
env.ECHO_WEBHOOK_SECRET, (err, app) => {
if(err) {
cb(err);
return;
}

if(env.PORT) {
// In a hosting environment like Bluemix for example, HTTPS is
// handled by a reverse proxy in front of the app, just listen
// on the configured HTTP port
log('HTTP server listening on port %d', env.PORT);
http.createServer(app).listen(env.PORT, cb);
}

else
// Listen on the configured HTTPS port, default to 443
ssl.conf(env, (err, conf) => {
if(err) {
cb(err);
return;
}
const port = env.SSLPORT || 443;
log('HTTPS server listening on port %d', port);
https.createServer(conf, app).listen(port, cb);
});
});
const main = async (argv, env) => {
try {
// Create Express Web app
const app = await webapp(
env.ECHO_APP_ID, env.ECHO_APP_SECRET,
env.ECHO_WEBHOOK_SECRET);
if(env.PORT) {
// In a hosting environment like Bluemix for example, HTTPS is
// handled by a reverse proxy in front of the app, just listen
// on the configured HTTP port
log('HTTP server listening on port %d', env.PORT);
http.createServer(app).listen(env.PORT);
}

else {

// Listen on the configured HTTPS port, default to 443
const sslConfig = await ssl.conf(env);
const port = env.SSLPORT || 443;
log('HTTPS server listening on port %D', port);
https.createServer(sslConfig, app).listen(port);
}
}
catch(err) {
throw err;
}
};

if (require.main === module)
main(process.argv, process.env, (err) => {
if(err) {
// Run main as IIFE (top level await not supported)
(async () => {
if (require.main === module)
try {
await main(process.argv, process.env);
log('App started!');
}
catch(err) {
console.log('Error starting app:', err);
return;
}
log('App started');
});
})();


48 changes: 25 additions & 23 deletions src/oauth.js
Original file line number Diff line number Diff line change
@@ -1,29 +1,32 @@
// Regularly obtain a fresh OAuth token for the app

import * as request from 'request';
import * as jsonwebtoken from 'jsonwebtoken';
import debug from 'debug';
import Promise from 'bluebird';

// Setup debug log
const log = debug('watsonwork-echo-oauth');

// Promisify request's post function
const post = Promise.promisify(request.post);


// Obtain an OAuth token for the app, repeat at regular intervals before the
// token expires. Returns a function that will always return a current
// valid token.
export const run = (appId, secret, cb) => {
export const run = async (appId, secret) => {
let tok;

// Return the current token
const current = () => tok;

// Return the time to live of a token
const ttl = (tok) =>
const ttl = (tok) =>
Math.max(0, jsonwebtoken.decode(tok).exp * 1000 - Date.now());

// Refresh the token
const refresh = (cb) => {
const refresh = async () => {
log('Getting token');
request.post('https://api.watsonwork.ibm.com/oauth/token', {
const res = await post('https://api.watsonwork.ibm.com/oauth/token', {
auth: {
user: appId,
pass: secret
Expand All @@ -32,28 +35,27 @@ export const run = (appId, secret, cb) => {
form: {
grant_type: 'client_credentials'
}
}, (err, res) => {
if(err || res.statusCode !== 200) {
log('Error getting token %o', err || res.statusCode);
cb(err || new Error(res.statusCode));
return;
}
});

// Save the fresh token
log('Got new token');
tok = res.body.access_token;
// check the status code of the result
if (res.statusCode !== 200)
throw new Error(res.statusCode);

// Save the fresh token
log('Got new token');
tok = res.body.access_token;

// Schedule next refresh a bit before the token expires
const t = ttl(tok);
log('Token ttl', t);
setTimeout(refresh, Math.max(0, t - 60000)).unref();
// Schedule next refresh a bit before the token expires
const t = ttl(tok);
log('Token ttl', t);
setTimeout(refresh, Math.max(0, t - 60000)).unref();

// Return a function that'll return the current token
cb(undefined, current);
});

// return the current token
return current;
};

// Obtain initial token
setImmediate(() => refresh(cb));
return await refresh();
};

Loading