From 1d0dc4cf251fdea6130a89cfbd94b78202bb5db7 Mon Sep 17 00:00:00 2001 From: Rodrigo Rodriguez Date: Sat, 12 May 2018 13:40:34 -0300 Subject: [PATCH] Building 3rd party service webhook for Whatsapp.gblib. Signed-off-by: Rodrigo Rodriguez --- README.md | 43 ++-- deploy/core.gbapp/models/GBModel.ts | 2 + deploy/whatsapp.gblib/index.ts | 4 +- .../services/WhatsappDirectLine.ts | 206 ++++++++++-------- package.json | 2 +- 5 files changed, 145 insertions(+), 112 deletions(-) diff --git a/README.md b/README.md index b06c8e61..86e0d163 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,9 @@ -![General Bots Logo](https://raw.githubusercontent.com/pragmatismo-io/BotServer/master/logo.png) +![General Bot Logo](https://raw.githubusercontent.com/pragmatismo-io/BotServer/master/logo.png) -Welcome to General Bots Community Edition +Welcome to General Bot Community Edition ---------------- -General Bots is a package based chat bot server focused in convention +General Bot is a package based chat bot server focused in convention over configuration and code-less approaches, which brings software packages and application server concepts to help parallel bot development. @@ -31,7 +31,7 @@ How To ### Run the server locally -1. Install [Node.js](https://www.npmjs.com/get-npm) the current generation General Bots code execution platform; +1. Install [Node.js](https://www.npmjs.com/get-npm) the current generation General Bot code execution platform; 2. Open a **Terminal** on Linux and Mac or a **Command Prompt** window on Windows;npm 3. Type `npm install -g botserver` and press *ENTER*; 4. Type `gbot` to run the server core. @@ -39,7 +39,7 @@ How To Notes: * [*nodejs.install* Chocolatey Package](https://chocolatey.org/packages/nodejs.install) is also available. -* The zip source code of general bots is also available for [Download](https://codeload.github.com/pragmatismo-io/BotServer/zip/master); +* The zip source code of General Bot is also available for [Download](https://codeload.github.com/pragmatismo-io/BotServer/zip/master); ### Configure the server to deploy specific directory @@ -56,19 +56,21 @@ Note: 1. [Optional] Install [Chocolatey](https://chocolatey.org/install), a Windows Package Manager; 2. Install [git](`https://git-scm.com/`), a Software Configuration Management (SCM).; 3. Install [Node.js](npmjs.com/get-npm), a [Runtime system](https://en.wikipedia.org/wiki/Runtime_system). -(https://www.npmjs.com/get-npm); +(https://www.npmjs.com/get-npm) (suggested: LTS 8.x.x); 4. Install [Visual Studio Code](https://chocolatey.org/packages/nodejs.install), Brackets or Atom as an editor of your choice; 5. [Fork](https://en.wikipedia.org/wiki/Fork_(software_development)) by visiting https://github.com/pragmatismo-io/BotServer/fork 6. Clone the just forked repository by running `git clone /BotServer.git` ; -7. Run `npm install` on Command Prompt or PowerShell on the General Bots source-code folder; -8. Enter './deploy/default.gbui' folder; -9. Run `npm install` folled by `npm run build` (To build default Bot UI); -10. Enter the On the downloaded folder (../..); -11. Run the bot server by `npm start`. +7. Run `npm install -g typescript`; +8. Run `npm install` on Command Prompt or PowerShell on the General Bot source-code folder; +9. Enter './deploy/default.gbui' folder; +10. Run `npm install` folled by `npm run build` (To build default Bot UI); +11. Enter the On the downloaded folder (../..); +12. Compile the bot server by `tsc`. +13. Run the bot server by `npm start`. Note: -* Whenever you are ready to turn your open-source bot ideas in form of .gbapp (source-code) and artifacts like .gbkb, .gbtheme, .gbot or the .gbai full package read [CONTRIBUTING.md](https://github.com/pragmatismo-io/BotServer/blob/master/CONTRIBUTING.md) about performing Pull Requests (PR) and creating other public custom packages repositories of your own personal or organization General Bots Community Edition powered packages. +* Whenever you are ready to turn your open-source bot ideas in form of .gbapp (source-code) and artifacts like .gbkb, .gbtheme, .gbot or the .gbai full package read [CONTRIBUTING.md](https://github.com/pragmatismo-io/BotServer/blob/master/CONTRIBUTING.md) about performing Pull Requests (PR) and creating other public custom packages repositories of your own personal or organization General Bot Community Edition powered packages. ### Just copy the source code to your machine @@ -81,7 +83,7 @@ The subjects.json file contains all information related to the subject tree and ### Creating a new Theme folder (.gbtheme folder) A theme is composed of some CSS files and images. That set of files can change -everything in the General Bots UI. Use them extensively before going to change +everything in the General Bot UI. Use them extensively before going to change the UI application itself (HTML & JS). Package Types @@ -97,10 +99,10 @@ directory. The artificial intelligence extensions in form of pluggable apps. Dialogs, Services and all model related to data. A set of interactions, use cases, integrations in form of conversationals dialogs. -The .gbapp adds the General Bots base library (botlib) for building Node.js TypeScript Apps packages. +The .gbapp adds the General Bot base library (botlib) for building Node.js TypeScript Apps packages. -Four components builds up a General Bots App: +Four components builds up a General Bot App: * dialogs * models @@ -122,7 +124,6 @@ Models builds the foundation of data relationships in form of entities. Services are a façade for bot back-end logic and other custom processing. - #### Tests Tests try to automate code execution validation before crashing in production. @@ -152,7 +153,7 @@ Reference ### GeneralBots admin commands -General Bots can be controlled by the same chat window people talk to, so +General Bot can be controlled by the same chat window people talk to, so here is a list of admin commands related to deploying .gb* files. | Command | Description | @@ -167,17 +168,17 @@ here is a list of admin commands related to deploying .gb* files. * Rodrigo Rodriguez (me@rodrigorodriguez.com) - Coding, Docs & Architecture. * David Lerner (david.lerner@hotmail.com) - UI, UX & Theming * Eduardo Romeiro (eromeirosp@outlook.com) - Content & UX - +* Jorge Ramos (jramos@pobox.com) - Coding, Docs & Architecture. Powered by Microsoft [BOT Framework](https://dev.botframework.com/) and [Azure](http://www.azure.com). -General Bots Code Name is [Guaribas](https://en.wikipedia.org/wiki/Guaribas), the name of a city in Brasil, state of Piaui. +General Bot Code Name is [Guaribas](https://en.wikipedia.org/wiki/Guaribas), the name of a city in Brasil, state of Piaui. [Roberto Mangabeira Unger](http://www.robertounger.com/en/): "No one should have to do work that can be done by a machine". ## License & Warranty -General Bots Copyright (c) Pragmatismo.io. All rights reserved. +General Bot Copyright (c) Pragmatismo.io. All rights reserved. Licensed under the AGPL-3.0. According to our dual licensing model, this program can be used either @@ -193,7 +194,7 @@ but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. -"General Bots" is a registered trademark of Pragmatismo.io. +"General Bot" is a registered trademark of Pragmatismo.io. The licensing of the program under the AGPLv3 does not imply a trademark license. Therefore any rights, title and interest in our trademarks remain entirely with us. diff --git a/deploy/core.gbapp/models/GBModel.ts b/deploy/core.gbapp/models/GBModel.ts index 03b9f1f7..c5706bfa 100644 --- a/deploy/core.gbapp/models/GBModel.ts +++ b/deploy/core.gbapp/models/GBModel.ts @@ -95,6 +95,8 @@ export class GuaribasInstance extends Model implements IGBInst @Column whatsappServiceKey: string; + @Column whatsappServiceNumber: string; + @Column spellcheckerKey: string; @Column theme: string; diff --git a/deploy/whatsapp.gblib/index.ts b/deploy/whatsapp.gblib/index.ts index a96e1187..357465a9 100644 --- a/deploy/whatsapp.gblib/index.ts +++ b/deploy/whatsapp.gblib/index.ts @@ -42,6 +42,7 @@ import { WhatsappDirectLine } from "./services/WhatsappDirectLine"; export class GBWhatsappPackage implements IGBPackage { + sysPackages: IGBPackage[] = null; channel: WhatsappDirectLine; @@ -53,7 +54,8 @@ export class GBWhatsappPackage implements IGBPackage { } loadBot(min: GBMinInstance): void { - this.channel = new WhatsappDirectLine(min.instance.whatsappBotKey); + this.channel = new WhatsappDirectLine(min.botId, min.instance.whatsappBotKey, min.instance.whatsappServiceKey, + min.instance.whatsappServiceNumber); } unloadBot(min: GBMinInstance): void { diff --git a/deploy/whatsapp.gblib/services/WhatsappDirectLine.ts b/deploy/whatsapp.gblib/services/WhatsappDirectLine.ts index 733bc77f..1c0ae08d 100644 --- a/deploy/whatsapp.gblib/services/WhatsappDirectLine.ts +++ b/deploy/whatsapp.gblib/services/WhatsappDirectLine.ts @@ -47,18 +47,23 @@ import { GBServiceCallback, GBService, IGBInstance } from "botlib"; export class WhatsappDirectLine extends GBService { pollInterval = 1000; - directLineSecret = ''; directLineClientName = 'DirectLineClient'; directLineSpecUrl = 'https://docs.botframework.com/en-us/restapi/directline3/swagger.json'; + directLineClient: any; + whatsappServiceKey: string; + whatsappServiceNumber: string; + botId: string; + + constructor(botId, directLineSecret, whatsappServiceKey, whatsappServiceNumber) { - constructor(directLineSecret) { super(); - this.directLineSecret = directLineSecret; + this.botId = botId; + this.whatsappServiceKey = whatsappServiceKey; + this.whatsappServiceNumber = whatsappServiceNumber; - // TODO: Migrate to Swagger 3. - let directLineClient = rp(this.directLineSpecUrl) + this.directLineClient = rp(this.directLineSpecUrl) .then(function (spec) { return new Swagger({ spec: JSON.parse(spec.trim()), @@ -67,147 +72,173 @@ export class WhatsappDirectLine extends GBService { }) .then(function (client) { client.clientAuthorizations.add('AuthorizationBotConnector', - new Swagger.ApiKeyAuthorization('Authorization', 'Bearer ' + directLineSecret, 'header')); + new Swagger.ApiKeyAuthorization('Authorization', 'Bearer ' + + directLineSecret, 'header')); return client; }) .catch(function (err) { - console.error('Error initializing DirectLine client', err); + logger.error('Error initializing DirectLine client', err); }); - // TODO: Remove *this* issue. - let _this = this; - directLineClient.then(function (client) { - client.Conversations.Conversations_StartConversation() - .then(function (response) { - return response.obj.conversationId; - }) - .then(function (conversationId) { - _this.sendMessagesFromConsole(client, conversationId); - _this.pollMessages(client, conversationId); - }) - .catch(function (err) { - console.error('Error starting conversation', err); - }); - }); } - received (req,res){ - logger.info(`Whatsapp called. Event: ${req.body.event}, muid: ${req.body.muid}, cuid: ${req.body.cuid}`); - res.end(); - } + received(req, res) { - sendMessagesFromConsole(client, conversationId) { - let _this = this; - var stdin = process.openStdin(); - process.stdout.write('Command> '); - stdin.addListener('data', function (e) { - var input = e.toString().trim(); - if (input) { - // exit - if (input.toLowerCase() === 'exit') { - return process.exit(); - } + logger.info(`GBWhatsapp: Hook called. Event: ${req.body.event}, + muid: ${req.body.muid}, contact: ${req.body.contact.name}, + (${req.body.contact.uid}, ${req.body.contact.type})`); - client.Conversations.Conversations_PostActivity( - { - conversationId: conversationId, - activity: { - textFormat: 'plain', - text: input, - type: 'message', - from: { - id: _this.directLineClientName, - name: _this.directLineClientName - } - } - }).catch(function (err) { - console.error('Error sending message:', err); + let conversationId = null; // req.body.cuid; + let text = req.body.message.body.text; + let from = req.body.contact.uid; + let fromName = req.body.contact.name; + + this.directLineClient.then((client) => { + + if (conversationId == null) { + + logger.info(`GBWhatsapp: Starting new conversation on Bot.`); + client.Conversations.Conversations_StartConversation() + .then((response) => { + return response.obj.conversationId; + }) + .then((conversationId) => { + + this.inputMessage(client, conversationId, text, + from, fromName); + + this.pollMessages(client, conversationId,from, fromName); + }) + .catch((err) => { + console.error('Error starting conversation', err); }); - process.stdout.write('Command> '); + } else { + this.inputMessage(client, conversationId, text, + from, fromName); + + this.pollMessages(client, conversationId, from, fromName); } + res.end(); }); } - /** TBD: Poll Messages from conversation using DirectLine client */ - pollMessages(client, conversationId) { - let _this = this; - console.log('Starting polling message for conversationId: ' + conversationId); + + inputMessage(client, conversationId, text, from, fromName) { + + client.Conversations.Conversations_PostActivity( + { + conversationId: conversationId, + activity: { + textFormat: 'plain', + text: text, + type: 'message', + from: { + id: from, + name: fromName + }, + replyToId: from + } + }).catch(function (err) { + logger.error(`GBWhatsapp: Error receiving message: ${err}.`); + }); + + } + + + pollMessages(client, conversationId,from, fromName){ + + logger.info(`GBWhatsapp: Starting polling message for conversationId: + ${conversationId}.`); + var watermark = null; - setInterval(function () { - client.Conversations.Conversations_GetActivities({ conversationId: conversationId, watermark: watermark }) - .then(function (response) { - watermark = response.obj.watermark; // use watermark so subsequent requests skip old messages + setInterval(() => { + client.Conversations.Conversations_GetActivities({ + conversationId: + conversationId, watermark: watermark + }) + .then((response) => { + watermark = response.obj.watermark; return response.obj.activities; }) - .then(_this.printMessages, _this.directLineClientName); + .then((activities) => { + this.printMessages(activities, from, fromName); + }); }, this.pollInterval); } - printMessages(activities, directLineClientName) { + printMessages(activities,from, fromName) { if (activities && activities.length) { - // ignore own messages - activities = activities.filter(function (m) { return m.from.id !== directLineClientName }); + + // Ignore own messages. + + activities = activities.filter((m) => { return m.from.id === this.botId }); if (activities.length) { - // print other messages - activities.forEach(activity => { - console.log(activity.text); - }); + // Print other messages. - process.stdout.write('Command> '); + activities.forEach(activity => { + this.printMessage(activity, from, fromName); + }); } } } - printMessage(activity) { + printMessage(activity, from, fromName) { + + let output: string; + if (activity.text) { - console.log(activity.text); + logger.info(`GBWhatsapp: MSG: ${activity.text}`); + output = activity.text; } if (activity.attachments) { activity.attachments.forEach(function (attachment) { switch (attachment.contentType) { case "application/vnd.microsoft.card.hero": - this.renderHeroCard(attachment); + output += this.renderHeroCard(attachment); break; case "image/png": - console.log('Opening the requested image ' + attachment.contentUrl); - open(attachment.contentUrl); + logger.info('Opening the requested image ' + attachment.contentUrl); + output += `\n${attachment.contentUrl}`; break; } }); } + + this.sendToDevice(from, fromName, output); } renderHeroCard(attachment) { - var width = 70; - var contentLine = function (content) { + let output: string; + let width = 70; + + let contentLine = function (content) { return ' '.repeat((width - content.length) / 2) + content + ' '.repeat((width - content.length) / 2); } - console.log('/' + '*'.repeat(width + 1)); - console.log('*' + contentLine(attachment.content.title) + '*'); - console.log('*' + ' '.repeat(width) + '*'); - console.log('*' + contentLine(attachment.content.text) + '*'); - console.log('*'.repeat(width + 1) + '/'); + output += '/' + '*'.repeat(width + 1); + output += '*' + contentLine(attachment.content.title) + '*'; + output += '*' + ' '.repeat(width) + '*'; + output += '*' + contentLine(attachment.content.text) + '*'; + output += '*'.repeat(width + 1) + '/'; } - - async sendToDevice(senderID, msg) { + async sendToDevice(to, toName, msg) { var options = { method: 'POST', url: 'https://www.waboxapp.com/api/send/chat', qs: { - token: '', - uid: '55****388**', - to: senderID, + token: this.whatsappServiceKey, + uid: this.whatsappServiceNumber, + to: to, custom_uid: 'GBZAP' + (new Date()).toISOString, text: msg }, @@ -218,8 +249,5 @@ export class WhatsappDirectLine extends GBService { }; const result = await request.get(options); - } - - } \ No newline at end of file diff --git a/package.json b/package.json index 48aa256e..56c84c08 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,7 @@ "async": "^1.5.2", "body-parser": "^1.18.2", "botbuilder": "^3.14.0", - "botlib": "0.0.16", + "botlib": "0.0.18", "chai": "^4.1.2", "chokidar": "^2.0.2", "csv-parse": "^2.4.0",