diff --git a/lib/bot-builder.js b/lib/bot-builder.js index 089e8dd..90e2726 100644 --- a/lib/bot-builder.js +++ b/lib/bot-builder.js @@ -11,6 +11,7 @@ const groupmeSetup = require('./groupme/setup'); const lineSetup = require('./line/setup'); const viberSetup = require('./viber/setup'); const alexaSetup = require('./alexa/setup'); +const googleHangoutsSetup = require('./google-hangouts-chat/setup'); const fbTemplate = require('./facebook/format-message'); const slackTemplate = require('./slack/format-message'); const slackDialog = require('./slack/format-dialog'); @@ -68,6 +69,9 @@ module.exports = function botBuilder(messageHandler, options, optionalLogError) if (isEnabled('alexa')) { alexaSetup(api, messageHandlerPromise, logError); } + if (isEnabled('google-hangouts-chat')) { + googleHangoutsSetup(api, messageHandlerPromise, logError); + } return api; }; diff --git a/lib/google-hangouts-chat/parse.js b/lib/google-hangouts-chat/parse.js new file mode 100644 index 0000000..ed3c6d7 --- /dev/null +++ b/lib/google-hangouts-chat/parse.js @@ -0,0 +1,33 @@ +'use strict'; + +module.exports = function googleHangoutsParse(messageObject) { + if (!messageObject || !messageObject.type) return; + + const message = messageObject.message; + + const text = message && message.text ? message.text : ''; + + let messageType = 'unknown'; + switch (messageObject.type) { + case 'MESSAGE': + messageType = 'message'; + break; + case 'ADDED_TO_SPACE': + messageType = 'add-command'; + break; + case 'REMOVED_FROM_SPACE': + messageType = 'remove-command'; + break; + } + + const sender = messageType === 'message' ? + message && message.sender && message.sender.name : + (messageObject.user && messageObject.user.name || 'no sender'); + + return { + sender: sender, + text: text, + originalRequest: messageObject, + type: `google-hangouts-chat-${messageType}` + }; +}; diff --git a/lib/google-hangouts-chat/reply.js b/lib/google-hangouts-chat/reply.js new file mode 100644 index 0000000..76e6c44 --- /dev/null +++ b/lib/google-hangouts-chat/reply.js @@ -0,0 +1,11 @@ +'use strict'; + +module.exports = function googleHangoutsReply(botResponse) { + if (typeof botResponse === 'string') { + return { + text: botResponse + }; + } + + return botResponse; +}; diff --git a/lib/google-hangouts-chat/setup.js b/lib/google-hangouts-chat/setup.js new file mode 100644 index 0000000..2bde1ec --- /dev/null +++ b/lib/google-hangouts-chat/setup.js @@ -0,0 +1,44 @@ +'use strict'; + +const prompt = require('souffleur'); +const googleHangoutsParse = require('./parse'); +const googleHangoutsReply = require('./reply'); +const color = require('../console-colors'); +const envUtils = require('../utils/env-utils'); + +module.exports = function googleHangoutsSetup(api, bot, logError, optionalParser, optionalResponder) { + let parser = optionalParser || googleHangoutsParse; + let responder = optionalResponder || googleHangoutsReply; + + api.post('/google-hangouts-chat', request => { + return bot(parser(request.body), request) + .then(botReply => responder(botReply, envUtils.decode(request.env.googleHangoutsAppName))) + .catch(logError); + }); + + api.addPostDeployStep('google-hangouts-chat', (options, lambdaDetails, utils) => { + return Promise.resolve().then(() => { + if (options['configure-google-hangouts-chat-bot']) { + console.log(`\n\n${color.green}Google Hangouts Chat bot setup${color.reset}\n`); + console.log(`\nConfigure your Google Hangouts Chat bot endpoint to HTTPS and set this URL:.\n`); + console.log(`\n${color.cyan}${lambdaDetails.apiUrl}/google-hangouts-chat${color.reset}\n`); + + return prompt(['Google Hangouts Chat bot name']) + .then(results => { + const deployment = { + restApiId: lambdaDetails.apiId, + stageName: lambdaDetails.alias, + variables: { + googleHangoutsAppName: envUtils.encode(results['Google Hangouts Chat bot name']) + } + }; + + console.log(`\n`); + + return utils.apiGatewayPromise.createDeploymentPromise(deployment); + }); + } + }) + .then(() => `${lambdaDetails.apiUrl}/google-hangouts-chat`); + }); +}; diff --git a/spec/google-hangouts-chat/hangouts-chat-parse-spec.js b/spec/google-hangouts-chat/hangouts-chat-parse-spec.js new file mode 100644 index 0000000..00d13ae --- /dev/null +++ b/spec/google-hangouts-chat/hangouts-chat-parse-spec.js @@ -0,0 +1,58 @@ +/*global describe, it, expect, require */ +'use strict'; + +const parse = require('../../lib/google-hangouts-chat/parse'); + +describe('Google Hangout Chat parse', () => { + it('should return nothing if the format is invalid', () => { + expect(parse('string')).toBeUndefined(); + expect(parse()).toBeUndefined(); + expect(parse(false)).toBeUndefined(); + expect(parse(123)).toBeUndefined(); + expect(parse({})).toBeUndefined(); + expect(parse([1, 2, 3])).toBeUndefined(); + }); + it('should return undefined if the session user is missing', () => { + expect(parse({message: {}})).toBeUndefined(); + }); + it('should return original request with an empty text if the intent is missing', () => { + let msg = {message: {foo: 'bar'}, type: 'ADDED_TO_SPACE'}; + expect(parse(msg)).toEqual({ sender: 'no sender', text: '', originalRequest: msg, type: 'google-hangouts-chat-add-command'}); + }); + it('should return original request with an empty text if the intent name is missing', () => { + let msg = {message: {foo: 'bar'}, type: 'REMOVED_FROM_SPACE'}; + expect(parse(msg)).toEqual({ sender: 'no sender', text: '', originalRequest: msg, type: 'google-hangouts-chat-remove-command'}); + }); + it('should return a parsed object with proper sender and text when the intent name and session user are present', () => { + const msg = { + 'type': 'MESSAGE', + 'eventTime': '2017-03-02T19:02:59.910959Z', + 'space': { + 'name': 'spaces/AAAAAAAAAAA', + 'displayName': 'Ramdom Discussion Room', + 'type': 'ROOM' + }, + 'message': { + 'name': 'spaces/AAAAAAAAAAA/messages/CCCCCCCCCCC', + 'sender': { + 'name': 'users/12345678901234567890', + 'displayName': 'John Doe', + 'avatarUrl': 'https://lh3.googleusercontent.com/.../photo.jpg', + 'email': 'john@example.com' + }, + 'createTime': '2017-03-02T19:02:59.910959Z', + 'text': 'Hello World', + 'thread': { + 'name': 'spaces/AAAAAAAAAAA/threads/BBBBBBBBBBB' + } + } + }; + + expect(parse(msg)).toEqual({ + sender: 'users/12345678901234567890', + text: 'Hello World', + originalRequest: msg, + type: 'google-hangouts-chat-message' + }); + }); +}); diff --git a/spec/google-hangouts-chat/hangouts-chat-reply-spec.js b/spec/google-hangouts-chat/hangouts-chat-reply-spec.js new file mode 100644 index 0000000..c68fc15 --- /dev/null +++ b/spec/google-hangouts-chat/hangouts-chat-reply-spec.js @@ -0,0 +1,19 @@ +/*global describe, it, expect, require */ +'use strict'; +const reply = require('../../lib/google-hangouts-chat/reply'); + +describe('Google Hangouts Chat Reply', () => { + + it('just returns the bot response when its not a string', () => { + expect(reply()).toEqual(undefined); + expect(reply(undefined, 'Google Hangouts Chat Bot')).toEqual(undefined); + expect(reply({ hello: 'Google Hangouts Chat Bot'}, 'Google Hangouts Chat Bot')).toEqual({ hello: 'Google Hangouts Chat Bot'}); + }); + + it('just returns the proper Google Hangouts Chat response when its not a string', () => { + expect(reply('Google Hangouts Chat Bot', 'Google Hangouts Chat Bot')) + .toEqual({ + text: 'Google Hangouts Chat Bot' + }); + }); +}); diff --git a/spec/google-hangouts-chat/hangouts-chat-setup-spec.js b/spec/google-hangouts-chat/hangouts-chat-setup-spec.js new file mode 100644 index 0000000..d9ee3fe --- /dev/null +++ b/spec/google-hangouts-chat/hangouts-chat-setup-spec.js @@ -0,0 +1,141 @@ +/*global require, describe, it, expect, beforeEach, jasmine*/ +'use strict'; +const underTest = require('../../lib/google-hangouts-chat/setup'), + utils = require('../../lib/utils/env-utils'); +describe('Google Hangouts Chat setup', () => { + let api, bot, logError, parser, responder, botPromise, botResolve, botReject; + beforeEach(() => { + api = jasmine.createSpyObj('api', ['get', 'post', 'addPostDeployStep']); + botPromise = new Promise((resolve, reject) => { + botResolve = resolve; + botReject = reject; + }); + bot = jasmine.createSpy().and.returnValue(botPromise); + parser = jasmine.createSpy(); + logError = jasmine.createSpy(); + responder = jasmine.createSpy(); + underTest(api, bot, logError, parser, responder); + }); + describe('message processor', () => { + const singleMessageTemplate = { + 'type': 'MESSAGE', + 'eventTime': '2017-03-02T19:02:59.910959Z', + 'space': { + 'name': 'spaces/AAAAAAAAAAA', + 'displayName': 'Ramdom Discussion Room', + 'type': 'ROOM' + }, + 'message': { + 'name': 'spaces/AAAAAAAAAAA/messages/CCCCCCCCCCC', + 'sender': { + 'name': 'users/12345678901234567890', + 'displayName': 'John Doe', + 'avatarUrl': 'https://lh3.googleusercontent.com/.../photo.jpg', + 'email': 'john@example.com' + }, + 'createTime': '2017-03-02T19:02:59.910959Z', + 'text': 'Hello World', + 'thread': { + 'name': 'spaces/AAAAAAAAAAA/threads/BBBBBBBBBBB' + } + } + }; + it('wires the POST request for Google Hangouts Chat to the message processor', () => { + expect(api.post.calls.count()).toEqual(1); + expect(api.post).toHaveBeenCalledWith('/google-hangouts-chat', jasmine.any(Function)); + }); + describe('processing a single message', () => { + let handler; + beforeEach(() => { + handler = api.post.calls.argsFor(0)[1]; + }); + it('breaks down the message and puts it into the parser', () => { + handler({body: singleMessageTemplate, env: {googleHangoutsAppName: 'Google Hangouts Chat Bot'}}); + expect(parser).toHaveBeenCalledWith(singleMessageTemplate); + }); + it('passes the parsed value to the bot if a message can be parsed', (done) => { + parser.and.returnValue('MSG1'); + handler({body: singleMessageTemplate, env: {}}); + Promise.resolve().then(() => { + expect(bot).toHaveBeenCalledWith('MSG1', { body: singleMessageTemplate, env: {} }); + }).then(done, done.fail); + }); + it('responds when the bot resolves', (done) => { + parser.and.returnValue({sender: 'user1', text: 'MSG1', type: 'google-hangouts-chat-message'}); + botResolve('Hello Google Hangouts'); + handler({body: singleMessageTemplate, env: {googleHangoutsAppName: utils.encode('Google Hangouts Bot')}}).then(() => { + expect(responder).toHaveBeenCalledWith('Hello Google Hangouts', 'Google Hangouts Bot'); + }).then(done, done.fail); + }); + it('can work with bot responses as strings', (done) => { + botResolve('Hello Google Hangouts'); + parser.and.returnValue({sender: 'user1', text: 'Hello'}); + handler({body: singleMessageTemplate, env: {googleHangoutsAppName: utils.encode('Google Hangouts Bot')}}).then(() => { + expect(responder).toHaveBeenCalledWith('Hello Google Hangouts', 'Google Hangouts Bot'); + }).then(done, done.fail); + + }); + it('logs error when the bot rejects without responding', (done) => { + parser.and.returnValue('MSG1'); + + handler({body: singleMessageTemplate, env: {googleHangoutsAppName: 'Google Hangouts Bot'}}).then(() => { + expect(responder).not.toHaveBeenCalled(); + expect(logError).toHaveBeenCalledWith('No No'); + }).then(done, done.fail); + + botReject('No No'); + }); + it('logs the error when the responder throws an error', (done) => { + parser.and.returnValue('MSG1'); + responder.and.throwError('XXX'); + botResolve('Yes'); + handler({body: singleMessageTemplate, env: {googleHangoutsAppName: 'Google Hangouts Bot'}}).then(() => { + expect(logError).toHaveBeenCalledWith(jasmine.any(Error)); + }).then(done, done.fail); + }); + describe('working with promises in responders', () => { + let responderResolve, responderReject, responderPromise, hasResolved; + beforeEach(() => { + responderPromise = new Promise((resolve, reject) => { + responderResolve = resolve; + responderReject = reject; + }); + responder.and.returnValue(responderPromise); + + parser.and.returnValue('MSG1'); + }); + it('waits for the responders to resolve before completing the request', (done) => { + handler({body: singleMessageTemplate, env: {googleHangoutsAppName: 'Google Hangouts Bot'}}).then(() => { + hasResolved = true; + }); + + botPromise.then(() => { + expect(hasResolved).toBeFalsy(); + }).then(done, done.fail); + + botResolve('YES'); + }); + it('resolves when the responder resolves', (done) => { + handler({body: singleMessageTemplate, env: {googleHangoutsAppName: 'Google Hangouts Bot'}}).then((message) => { + expect(message).toEqual('As Promised!'); + }).then(done, done.fail); + + botPromise.then(() => { + responderResolve('As Promised!'); + }); + botResolve('YES'); + }); + it('logs error when the responder rejects', (done) => { + handler({body: singleMessageTemplate, env: {googleHangoutsAppName: 'Google Hangouts Bot'}}).then(() => { + expect(logError).toHaveBeenCalledWith('Bomb!'); + }).then(done, done.fail); + + botPromise.then(() => { + responderReject('Bomb!'); + }); + botResolve('YES'); + }); + }); + }); + }); +});