Building 3rd party service webhook for Whatsapp.gblib.

Signed-off-by: Rodrigo Rodriguez <me@rodrigorodriguez.com>
This commit is contained in:
Rodrigo Rodriguez 2018-05-12 13:40:34 -03:00
parent ba85db06dd
commit 1d0dc4cf25
5 changed files with 145 additions and 112 deletions
README.md
deploy
core.gbapp/models
whatsapp.gblib
package.json

View file

@ -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 over configuration and code-less approaches, which brings software packages
and application server concepts to help parallel bot development. and application server concepts to help parallel bot development.
@ -31,7 +31,7 @@ How To
### Run the server locally ### 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 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*; 3. Type `npm install -g botserver` and press *ENTER*;
4. Type `gbot` to run the server core. 4. Type `gbot` to run the server core.
@ -39,7 +39,7 @@ How To
Notes: Notes:
* [*nodejs.install* Chocolatey Package](https://chocolatey.org/packages/nodejs.install) is also available. * [*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 ### Configure the server to deploy specific directory
@ -56,19 +56,21 @@ Note:
1. [Optional] Install [Chocolatey](https://chocolatey.org/install), a Windows Package Manager; 1. [Optional] Install [Chocolatey](https://chocolatey.org/install), a Windows Package Manager;
2. Install [git](`https://git-scm.com/`), a Software Configuration Management (SCM).; 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). 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; 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 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 <your-forked-repository-url>/BotServer.git` ; 6. Clone the just forked repository by running `git clone <your-forked-repository-url>/BotServer.git` ;
7. Run `npm install` on Command Prompt or PowerShell on the General Bots source-code folder; 7. Run `npm install -g typescript`;
8. Enter './deploy/default.gbui' folder; 8. Run `npm install` on Command Prompt or PowerShell on the General Bot source-code folder;
9. Run `npm install` folled by `npm run build` (To build default Bot UI); 9. Enter './deploy/default.gbui' folder;
10. Enter the On the downloaded folder (../..); 10. Run `npm install` folled by `npm run build` (To build default Bot UI);
11. Run the bot server by `npm start`. 11. Enter the On the downloaded folder (../..);
12. Compile the bot server by `tsc`.
13. Run the bot server by `npm start`.
Note: 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 ### 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) ### Creating a new Theme folder (.gbtheme folder)
A theme is composed of some CSS files and images. That set of files can change 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). the UI application itself (HTML & JS).
Package Types Package Types
@ -97,10 +99,10 @@ directory.
The artificial intelligence extensions in form of pluggable apps. Dialogs, The artificial intelligence extensions in form of pluggable apps. Dialogs,
Services and all model related to data. A set of interactions, use cases, Services and all model related to data. A set of interactions, use cases,
integrations in form of conversationals dialogs. 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 * dialogs
* models * 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. Services are a façade for bot back-end logic and other custom processing.
#### Tests #### Tests
Tests try to automate code execution validation before crashing in production. Tests try to automate code execution validation before crashing in production.
@ -152,7 +153,7 @@ Reference
### GeneralBots admin commands ### 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. here is a list of admin commands related to deploying .gb* files.
| Command | Description | | 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. * Rodrigo Rodriguez (me@rodrigorodriguez.com) - Coding, Docs & Architecture.
* David Lerner (david.lerner@hotmail.com) - UI, UX & Theming * David Lerner (david.lerner@hotmail.com) - UI, UX & Theming
* Eduardo Romeiro (eromeirosp@outlook.com) - Content & UX * 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). 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". [Roberto Mangabeira Unger](http://www.robertounger.com/en/): "No one should have to do work that can be done by a machine".
## License & Warranty ## 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. Licensed under the AGPL-3.0.
According to our dual licensing model, this program can be used either 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 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details. 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 The licensing of the program under the AGPLv3 does not imply a
trademark license. Therefore any rights, title and interest in trademark license. Therefore any rights, title and interest in
our trademarks remain entirely with us. our trademarks remain entirely with us.

View file

@ -95,6 +95,8 @@ export class GuaribasInstance extends Model<GuaribasInstance> implements IGBInst
@Column whatsappServiceKey: string; @Column whatsappServiceKey: string;
@Column whatsappServiceNumber: string;
@Column spellcheckerKey: string; @Column spellcheckerKey: string;
@Column theme: string; @Column theme: string;

View file

@ -42,6 +42,7 @@ import { WhatsappDirectLine } from "./services/WhatsappDirectLine";
export class GBWhatsappPackage implements IGBPackage { export class GBWhatsappPackage implements IGBPackage {
sysPackages: IGBPackage[] = null; sysPackages: IGBPackage[] = null;
channel: WhatsappDirectLine; channel: WhatsappDirectLine;
@ -53,7 +54,8 @@ export class GBWhatsappPackage implements IGBPackage {
} }
loadBot(min: GBMinInstance): void { 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 { unloadBot(min: GBMinInstance): void {

View file

@ -47,18 +47,23 @@ import { GBServiceCallback, GBService, IGBInstance } from "botlib";
export class WhatsappDirectLine extends GBService { export class WhatsappDirectLine extends GBService {
pollInterval = 1000; pollInterval = 1000;
directLineSecret = '';
directLineClientName = 'DirectLineClient'; directLineClientName = 'DirectLineClient';
directLineSpecUrl = 'https://docs.botframework.com/en-us/restapi/directline3/swagger.json'; 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(); super();
this.directLineSecret = directLineSecret; this.botId = botId;
this.whatsappServiceKey = whatsappServiceKey;
this.whatsappServiceNumber = whatsappServiceNumber;
// TODO: Migrate to Swagger 3. // TODO: Migrate to Swagger 3.
let directLineClient = rp(this.directLineSpecUrl) this.directLineClient = rp(this.directLineSpecUrl)
.then(function (spec) { .then(function (spec) {
return new Swagger({ return new Swagger({
spec: JSON.parse(spec.trim()), spec: JSON.parse(spec.trim()),
@ -67,147 +72,173 @@ export class WhatsappDirectLine extends GBService {
}) })
.then(function (client) { .then(function (client) {
client.clientAuthorizations.add('AuthorizationBotConnector', client.clientAuthorizations.add('AuthorizationBotConnector',
new Swagger.ApiKeyAuthorization('Authorization', 'Bearer ' + directLineSecret, 'header')); new Swagger.ApiKeyAuthorization('Authorization', 'Bearer ' +
directLineSecret, 'header'));
return client; return client;
}) })
.catch(function (err) { .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) { received(req, res) {
logger.info(`Whatsapp called. Event: ${req.body.event}, muid: ${req.body.muid}, cuid: ${req.body.cuid}`);
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})`);
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);
});
} else {
this.inputMessage(client, conversationId, text,
from, fromName);
this.pollMessages(client, conversationId, from, fromName);
}
res.end(); res.end();
});
} }
sendMessagesFromConsole(client, conversationId) {
let _this = this; inputMessage(client, conversationId, text, from, fromName) {
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();
}
client.Conversations.Conversations_PostActivity( client.Conversations.Conversations_PostActivity(
{ {
conversationId: conversationId, conversationId: conversationId,
activity: { activity: {
textFormat: 'plain', textFormat: 'plain',
text: input, text: text,
type: 'message', type: 'message',
from: { from: {
id: _this.directLineClientName, id: from,
name: _this.directLineClientName name: fromName
} },
replyToId: from
} }
}).catch(function (err) { }).catch(function (err) {
console.error('Error sending message:', err); logger.error(`GBWhatsapp: Error receiving message: ${err}.`);
}); });
process.stdout.write('Command> ');
}
});
} }
/** TBD: Poll Messages from conversation using DirectLine client */
pollMessages(client, conversationId) { pollMessages(client, conversationId,from, fromName){
let _this = this;
console.log('Starting polling message for conversationId: ' + conversationId); logger.info(`GBWhatsapp: Starting polling message for conversationId:
${conversationId}.`);
var watermark = null; var watermark = null;
setInterval(function () { setInterval(() => {
client.Conversations.Conversations_GetActivities({ conversationId: conversationId, watermark: watermark }) client.Conversations.Conversations_GetActivities({
.then(function (response) { conversationId:
watermark = response.obj.watermark; // use watermark so subsequent requests skip old messages conversationId, watermark: watermark
})
.then((response) => {
watermark = response.obj.watermark;
return response.obj.activities; return response.obj.activities;
}) })
.then(_this.printMessages, _this.directLineClientName); .then((activities) => {
this.printMessages(activities, from, fromName);
});
}, this.pollInterval); }, this.pollInterval);
} }
printMessages(activities, directLineClientName) { printMessages(activities,from, fromName) {
if (activities && activities.length) { 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) { if (activities.length) {
// print other messages // Print other messages.
activities.forEach(activity => { activities.forEach(activity => {
console.log(activity.text); this.printMessage(activity, from, fromName);
}); });
process.stdout.write('Command> ');
} }
} }
} }
printMessage(activity) { printMessage(activity, from, fromName) {
let output: string;
if (activity.text) { if (activity.text) {
console.log(activity.text); logger.info(`GBWhatsapp: MSG: ${activity.text}`);
output = activity.text;
} }
if (activity.attachments) { if (activity.attachments) {
activity.attachments.forEach(function (attachment) { activity.attachments.forEach(function (attachment) {
switch (attachment.contentType) { switch (attachment.contentType) {
case "application/vnd.microsoft.card.hero": case "application/vnd.microsoft.card.hero":
this.renderHeroCard(attachment); output += this.renderHeroCard(attachment);
break; break;
case "image/png": case "image/png":
console.log('Opening the requested image ' + attachment.contentUrl); logger.info('Opening the requested image ' + attachment.contentUrl);
open(attachment.contentUrl); output += `\n${attachment.contentUrl}`;
break; break;
} }
}); });
} }
this.sendToDevice(from, fromName, output);
} }
renderHeroCard(attachment) { renderHeroCard(attachment) {
var width = 70; let output: string;
var contentLine = function (content) { let width = 70;
let contentLine = function (content) {
return ' '.repeat((width - content.length) / 2) + return ' '.repeat((width - content.length) / 2) +
content + content +
' '.repeat((width - content.length) / 2); ' '.repeat((width - content.length) / 2);
} }
console.log('/' + '*'.repeat(width + 1)); output += '/' + '*'.repeat(width + 1);
console.log('*' + contentLine(attachment.content.title) + '*'); output += '*' + contentLine(attachment.content.title) + '*';
console.log('*' + ' '.repeat(width) + '*'); output += '*' + ' '.repeat(width) + '*';
console.log('*' + contentLine(attachment.content.text) + '*'); output += '*' + contentLine(attachment.content.text) + '*';
console.log('*'.repeat(width + 1) + '/'); output += '*'.repeat(width + 1) + '/';
} }
async sendToDevice(to, toName, msg) {
async sendToDevice(senderID, msg) {
var options = { var options = {
method: 'POST', method: 'POST',
url: 'https://www.waboxapp.com/api/send/chat', url: 'https://www.waboxapp.com/api/send/chat',
qs: qs:
{ {
token: '', token: this.whatsappServiceKey,
uid: '55****388**', uid: this.whatsappServiceNumber,
to: senderID, to: to,
custom_uid: 'GBZAP' + (new Date()).toISOString, custom_uid: 'GBZAP' + (new Date()).toISOString,
text: msg text: msg
}, },
@ -218,8 +249,5 @@ export class WhatsappDirectLine extends GBService {
}; };
const result = await request.get(options); const result = await request.get(options);
} }
} }

View file

@ -32,7 +32,7 @@
"async": "^1.5.2", "async": "^1.5.2",
"body-parser": "^1.18.2", "body-parser": "^1.18.2",
"botbuilder": "^3.14.0", "botbuilder": "^3.14.0",
"botlib": "0.0.16", "botlib": "0.0.18",
"chai": "^4.1.2", "chai": "^4.1.2",
"chokidar": "^2.0.2", "chokidar": "^2.0.2",
"csv-parse": "^2.4.0", "csv-parse": "^2.4.0",