feat(whatsapp.gblib): Now Whatsapp will display markdown from .gbkb including images.

This commit is contained in:
Rodrigo Rodriguez 2019-08-24 18:46:04 -03:00
parent 246b2226bf
commit faa5ec710c
8 changed files with 218 additions and 96 deletions

View file

@ -63,7 +63,6 @@ export class SwitchBotDialog extends IGBDialog {
async step => { async step => {
let sec = new SecService(); let sec = new SecService();
let from = step.context.activity.from.id; let from = step.context.activity.from.id;
const minBoot = GBServer.globals.minInstances[0];
await sec.updateCurrentBotId(from, step.result); await sec.updateCurrentBotId(from, step.result);
await step.context.sendActivity(`Opa, vamos lá!`); await step.context.sendActivity(`Opa, vamos lá!`);

View file

@ -64,11 +64,17 @@ export class GBConversationalService implements IGBConversationalService {
} }
public async sendFile(min: GBMinInstance, step: GBDialogStep, url: string): Promise<any> { public async sendFile(min: GBMinInstance, step: GBDialogStep, url: string): Promise<any> {
let mobile = step.context.activity.from.id; const mobile = step.context.activity.from.id;
min.whatsAppDirectLine.sendFile(mobile, url); const filename = url.substring(url.lastIndexOf('/')+1);
await min.whatsAppDirectLine.sendFileToDevice(mobile, url, filename);
} }
public async sendAudio(min: GBMinInstance, step: GBDialogStep, url: string): Promise<any> {
const mobile = step.context.activity.from.id;
await min.whatsAppDirectLine.sendAudioToDevice(mobile, url);
}
public async sendEvent(step: GBDialogStep, name: string, value: Object): Promise<any> { public async sendEvent(step: GBDialogStep, name: string, value: Object): Promise<any> {
if (step.context.activity.channelId === 'webchat') { if (step.context.activity.channelId === 'webchat') {
const msg = MessageFactory.text(''); const msg = MessageFactory.text('');

View file

@ -203,6 +203,10 @@ export class GBDeployer {
instance.whatsappServiceNumber = bootInstance.whatsappServiceNumber; instance.whatsappServiceNumber = bootInstance.whatsappServiceNumber;
instance.whatsappServiceUrl = bootInstance.whatsappServiceUrl; instance.whatsappServiceUrl = bootInstance.whatsappServiceUrl;
instance.whatsappServiceWebhookUrl = bootInstance.whatsappServiceWebhookUrl; instance.whatsappServiceWebhookUrl = bootInstance.whatsappServiceWebhookUrl;
instance.storageServer = bootInstance.storageServer;
instance.storageName = bootInstance.storageName;
instance.storageUsername = bootInstance.storageUsername;
instance.storagePassword = bootInstance.storagePassword;
instance = await service.internalDeployBot( instance = await service.internalDeployBot(
instance, instance,
@ -422,9 +426,7 @@ export class GBDeployer {
server.use(`/themes/${filenameOnly}`, express.static(filename)); server.use(`/themes/${filenameOnly}`, express.static(filename));
GBLog.info(`Theme (.gbtheme) assets accessible at: /themes/${filenameOnly}.`); GBLog.info(`Theme (.gbtheme) assets accessible at: /themes/${filenameOnly}.`);
} else if (Path.extname(filename) === '.gbkb') { } else if (Path.extname(filename) === '.gbkb') {
server.use(`/kb/${filenameOnly}/subjects`, express.static(urlJoin(filename, 'subjects'))); this.mountGBKBAssets( filenameOnly, filename);
server.use(`/kb/${filenameOnly}/images`, express.static(urlJoin(filename, 'images')));
GBLog.info(`KB (.gbkb) assets accessible at: /kb/${filenameOnly}.`);
} else if (Path.extname(filename) === '.gbui') { } else if (Path.extname(filename) === '.gbui') {
// Already Handled // Already Handled
} else if (Path.extname(filename) === '.gbdialog') { } else if (Path.extname(filename) === '.gbdialog') {
@ -458,6 +460,13 @@ export class GBDeployer {
return { generalPackages, totalPackages }; return { generalPackages, totalPackages };
} }
private mountGBKBAssets(packageName: any, filename: string) {
GBServer.globals.server.use(`/kb/${packageName}/subjects`, express.static(urlJoin(filename, 'subjects')));
GBServer.globals.server.use(`/kb/${packageName}/images`, express.static(urlJoin(filename, 'images')));
GBServer.globals.server.use(`/kb/${packageName}/audios`, express.static(urlJoin(filename, 'audios')));
GBLog.info(`KB (.gbkb) assets accessible at: /kb/${packageName}.`);
}
private isSystemPackage(name: string): Boolean { private isSystemPackage(name: string): Boolean {
const names = ['core.gbapp', 'admin.gbapp', 'azuredeployer.gbapp', 'customer-satisfaction.gbapp', 'kb.gbapp']; const names = ['core.gbapp', 'admin.gbapp', 'azuredeployer.gbapp', 'customer-satisfaction.gbapp', 'kb.gbapp'];

View file

@ -138,9 +138,8 @@ export class GBMinService {
return; // Exit here. return; // Exit here.
} }
const minBoot = GBServer.globals.bootInstance;
const toSwitchMin = GBServer.globals.minInstances.filter(p => p.botId === text)[0]; const toSwitchMin = GBServer.globals.minInstances.filter(p => p.botId === text)[0];
let activeMin = toSwitchMin ? toSwitchMin : minBoot; let activeMin = toSwitchMin ? toSwitchMin : GBServer.globals.minBoot;
let sec = new SecService(); let sec = new SecService();
let user = await sec.getUserFromPhone(id); let user = await sec.getUserFromPhone(id);
@ -190,6 +189,10 @@ export class GBMinService {
// Build bot adapter. // Build bot adapter.
const { min, adapter, conversationState } = await this.buildBotAdapter(instance, GBServer.globals.publicAddress, GBServer.globals.sysPackages); const { min, adapter, conversationState } = await this.buildBotAdapter(instance, GBServer.globals.publicAddress, GBServer.globals.sysPackages);
if (GBServer.globals.minInstances.length === 0) {
GBServer.globals.minBoot = min;
}
GBServer.globals.minInstances.push(min); GBServer.globals.minInstances.push(min);
// Install default VBA module. // Install default VBA module.
@ -453,11 +456,13 @@ export class GBMinService {
user.cb = undefined; user.cb = undefined;
await min.userProfile.set(step.context, user); await min.userProfile.set(step.context, user);
let sec = new SecService(); if (context.activity.membersAdded !== undefined) {
const member = context.activity.membersAdded[0]; let sec = new SecService();
const member = context.activity.membersAdded[0];
await sec.ensureUser(instance.instanceId, member.id, await sec.ensureUser(instance.instanceId, member.id,
min.botId, member.id, "", "web", member.name, member.id); min.botId, member.id, "", "web", member.name, member.id);
}
} }
GBLog.info( GBLog.info(

View file

@ -144,7 +144,7 @@ export class AskDialog extends IGBDialog {
// Sends the answer to all outputs, including projector. // Sends the answer to all outputs, including projector.
await service.sendAnswer(AskDialog.getChannel(step), min.conversationalService, step, resultsA.answer); await service.sendAnswer(min, AskDialog.getChannel(step), step, resultsA.answer);
// Goes to ask loop, again. // Goes to ask loop, again.
return await step.replaceDialog('/ask', { isReturning: true }); return await step.replaceDialog('/ask', { isReturning: true });
@ -164,7 +164,7 @@ export class AskDialog extends IGBDialog {
await step.context.sendActivity(Messages[locale].wider_answer); await step.context.sendActivity(Messages[locale].wider_answer);
} }
// Sends the answer to all outputs, including projector. // Sends the answer to all outputs, including projector.
await service.sendAnswer(AskDialog.getChannel(step), min.conversationalService, step, resultsB.answer); await service.sendAnswer(min, AskDialog.getChannel(step), step, resultsB.answer);
return await step.replaceDialog('/ask', { isReturning: true }); return await step.replaceDialog('/ask', { isReturning: true });
} else { } else {
@ -180,7 +180,7 @@ export class AskDialog extends IGBDialog {
} }
private static getChannel(step): string { private static getChannel(step): string {
return Number.isInteger(step.context.activity.from.id) ? 'whatsapp' : step.context.activity.channelId; return !isNaN(step.context.activity.from.id) ? 'whatsapp' : step.context.activity.channelId;
} }
private static getAnswerEventDialog(service: KBService, min: GBMinInstance) { private static getAnswerEventDialog(service: KBService, min: GBMinInstance) {
@ -191,7 +191,7 @@ export class AskDialog extends IGBDialog {
const question = await service.getQuestionById(min.instance.instanceId, data.questionId); const question = await service.getQuestionById(min.instance.instanceId, data.questionId);
const answer = await service.getAnswerById(min.instance.instanceId, question.answerId); const answer = await service.getAnswerById(min.instance.instanceId, question.answerId);
// Sends the answer to all outputs, including projector. // Sends the answer to all outputs, including projector.
await service.sendAnswer(AskDialog.getChannel(step), min.conversationalService, step, answer); await service.sendAnswer(min, AskDialog.getChannel(step), step, answer);
await step.replaceDialog('/ask', { isReturning: true }); await step.replaceDialog('/ask', { isReturning: true });
} }

View file

@ -46,7 +46,7 @@ const walkPromise = require('walk-promise');
const parse = require('bluebird').promisify(require('csv-parse')); const parse = require('bluebird').promisify(require('csv-parse'));
const { SearchService } = require('azure-search-client'); const { SearchService } = require('azure-search-client');
import { GBDialogStep, GBLog, IGBConversationalService, IGBCoreService, IGBInstance } from 'botlib'; import { GBDialogStep, GBLog, IGBConversationalService, IGBCoreService, IGBInstance, GBMinInstance } from 'botlib';
import { Sequelize } from 'sequelize-typescript'; import { Sequelize } from 'sequelize-typescript';
import { AzureDeployerService } from '../../azuredeployer.gbapp/services/AzureDeployerService'; import { AzureDeployerService } from '../../azuredeployer.gbapp/services/AzureDeployerService';
import { GuaribasPackage } from '../../core.gbapp/models/GBModel'; import { GuaribasPackage } from '../../core.gbapp/models/GBModel';
@ -54,6 +54,8 @@ import { GBDeployer } from '../../core.gbapp/services/GBDeployer';
import { GuaribasAnswer, GuaribasQuestion, GuaribasSubject } from '../models'; import { GuaribasAnswer, GuaribasQuestion, GuaribasSubject } from '../models';
import { Messages } from '../strings'; import { Messages } from '../strings';
import { GBConfigService } from './../../core.gbapp/services/GBConfigService'; import { GBConfigService } from './../../core.gbapp/services/GBConfigService';
import { GBServer } from '../../../src/app';
/** /**
* Result for quey on KB data. * Result for quey on KB data.
@ -349,21 +351,28 @@ export class KBService {
}); });
} }
public async sendAnswer(channel: string, conversationalService: IGBConversationalService, step: GBDialogStep, answer: GuaribasAnswer) { public async sendAnswer(min: GBMinInstance, channel: string, step: GBDialogStep, answer: GuaribasAnswer) {
if (answer.content.endsWith('.mp4')) { if (answer.content.endsWith('.mp4')) {
await this.playVideo(conversationalService, step, answer); await this.playVideo(min.conversationalService, step, answer);
} }
else if (answer.format === '.md') { else if (answer.format === '.md') {
await this.playMarkdown(answer, channel, step, conversationalService); await this.playMarkdown(min, answer, channel, step, min.conversationalService);
} else if (answer.content.endsWith('.ogg')) {
await this.playAudio(min, answer, channel, step, min.conversationalService);
} else { } else {
await step.context.sendActivity(answer.content); await step.context.sendActivity(answer.content);
await conversationalService.sendEvent(step, 'stop', undefined); await min.conversationalService.sendEvent(step, 'stop', undefined);
} }
} }
private async playMarkdown(answer: GuaribasAnswer, channel: string, step: GBDialogStep, conversationalService: IGBConversationalService) { private async playAudio(min: GBMinInstance, answer: GuaribasAnswer, channel: string, step: GBDialogStep, conversationalService: IGBConversationalService) {
conversationalService.sendAudio(min, step, answer.content);
}
private async playMarkdown(min: GBMinInstance, answer: GuaribasAnswer, channel: string, step: GBDialogStep, conversationalService: IGBConversationalService) {
let html = answer.content; let html = answer.content;
marked.setOptions({ marked.setOptions({
renderer: new marked.Renderer(), renderer: new marked.Renderer(),
@ -392,12 +401,82 @@ export class KBService {
}); });
} }
else if (channel === 'whatsapp') { else if (channel === 'whatsapp') {
let from = step.context.activity.from.id; await this.sendMarkdownToMobile(step, answer, conversationalService, min);
//conversationalService.sendFile(min, from, answer.content);
} }
} }
private async playVideo(conversationalService: IGBConversationalService, step: GBDialogStep, answer: GuaribasAnswer) { private async sendMarkdownToMobile(step: GBDialogStep, answer: GuaribasAnswer, conversationalService: IGBConversationalService, min: GBMinInstance) {
let text = answer.content;
enum State {
InText,
InImage,
InImageBegin,
InImageCaption,
InImageAddressBegin,
InImageAddressBody
};
let state = State.InText;
let currentImage = '';
let currentText = '';
//![General Bots](/instance/images/gb.png)
for (var i = 0; i < text.length; i++) {
const c = text.charAt(i);
switch (state) {
case State.InText:
if (c === '!') {
state = State.InImageBegin;
}
else {
currentText = currentText.concat(c);
}
break;
case State.InImageBegin:
if (c === '[') {
if (currentText !== '') {
await step.context.sendActivity(currentText);
}
currentText = '';
state = State.InImageCaption;
}
else {
state = State.InText;
currentText = currentText.concat('!').concat(c);
}
break;
case State.InImageCaption:
if (c === ']') {
state = State.InImageAddressBegin;
}
break;
case State.InImageAddressBegin:
if (c === '(') {
state = State.InImageAddressBody;
}
break;
case State.InImageAddressBody:
if (c === ')') {
state = State.InText;
let url = urlJoin(GBServer.globals.publicAddress, currentImage);
await conversationalService.sendFile(min, step, url);
}
else {
currentImage = currentImage.concat(c);
}
break;
}
}
if (currentText !== '') {
await step.context.sendActivity(currentText);
}
}
private async playVideo(conversationalService: IGBConversationalService, step: GBDialogStep, answer: GuaribasAnswer) {
await conversationalService.sendEvent(step, 'play', { await conversationalService.sendEvent(step, 'play', {
playerType: 'video', playerType: 'video',
data: answer.content data: answer.content
@ -405,74 +484,74 @@ private async playVideo(conversationalService: IGBConversationalService, step: G
} }
public async importKbPackage( public async importKbPackage(
localPath: string, localPath: string,
packageStorage: GuaribasPackage, packageStorage: GuaribasPackage,
instance: IGBInstance instance: IGBInstance
): Promise < any > { ): Promise<any> {
// Imports subjects tree into database and return it. // Imports subjects tree into database and return it.
await this.importSubjectFile(packageStorage.packageId, urlJoin(localPath, 'subjects.json'), instance); await this.importSubjectFile(packageStorage.packageId, urlJoin(localPath, 'subjects.json'), instance);
// Import all .tsv files in the tabular directory. // Import all .tsv files in the tabular directory.
return this.importKbTabularDirectory(localPath, instance, packageStorage.packageId); return this.importKbTabularDirectory(localPath, instance, packageStorage.packageId);
} }
public async importKbTabularDirectory(localPath: string, instance: IGBInstance, packageId: number): Promise < any > { public async importKbTabularDirectory(localPath: string, instance: IGBInstance, packageId: number): Promise<any> {
const files = await walkPromise(urlJoin(localPath, 'tabular')); const files = await walkPromise(urlJoin(localPath, 'tabular'));
return Promise.all( return Promise.all(
files.map(async file => { files.map(async file => {
if (file.name.endsWith('.xlsx')) { if (file.name.endsWith('.xlsx')) {
return this.importKbTabularFile(urlJoin(file.root, file.name), instance.instanceId, packageId); return this.importKbTabularFile(urlJoin(file.root, file.name), instance.instanceId, packageId);
} }
}) })
); );
} }
public async importSubjectFile(packageId: number, filename: string, instance: IGBInstance): Promise < any > { public async importSubjectFile(packageId: number, filename: string, instance: IGBInstance): Promise<any> {
const subjectsLoaded = JSON.parse(Fs.readFileSync(filename, 'utf8')); const subjectsLoaded = JSON.parse(Fs.readFileSync(filename, 'utf8'));
const doIt = async (subjects: GuaribasSubject[], parentSubjectId: number) => { const doIt = async (subjects: GuaribasSubject[], parentSubjectId: number) => {
return asyncPromise.eachSeries(subjects, async item => { return asyncPromise.eachSeries(subjects, async item => {
const value = await GuaribasSubject.create({ const value = await GuaribasSubject.create({
internalId: item.id, internalId: item.id,
parentSubjectId: parentSubjectId, parentSubjectId: parentSubjectId,
instanceId: instance.instanceId, instanceId: instance.instanceId,
from: item.from, from: item.from,
to: item.to, to: item.to,
title: item.title, title: item.title,
description: item.description, description: item.description,
packageId: packageId packageId: packageId
});
if (item.children) {
return Promise.resolve(doIt(item.children, value.subjectId));
} else {
return Promise.resolve(item);
}
}); });
};
if (item.children) { return doIt(subjectsLoaded.children, undefined);
return Promise.resolve(doIt(item.children, value.subjectId)); }
} else {
return Promise.resolve(item);
}
});
};
return doIt(subjectsLoaded.children, undefined);
}
public async undeployKbFromStorage(instance: IGBInstance, deployer: GBDeployer, packageId: number) { public async undeployKbFromStorage(instance: IGBInstance, deployer: GBDeployer, packageId: number) {
await GuaribasQuestion.destroy({ await GuaribasQuestion.destroy({
where: { instanceId: instance.instanceId, packageId: packageId } where: { instanceId: instance.instanceId, packageId: packageId }
}); });
await GuaribasAnswer.destroy({ await GuaribasAnswer.destroy({
where: { instanceId: instance.instanceId, packageId: packageId } where: { instanceId: instance.instanceId, packageId: packageId }
}); });
await GuaribasSubject.destroy({ await GuaribasSubject.destroy({
where: { instanceId: instance.instanceId, packageId: packageId } where: { instanceId: instance.instanceId, packageId: packageId }
}); });
await GuaribasPackage.destroy({ await GuaribasPackage.destroy({
where: { instanceId: instance.instanceId, packageId: packageId } where: { instanceId: instance.instanceId, packageId: packageId }
}); });
await deployer.rebuildIndex(instance, new AzureDeployerService(deployer).getKBSearchSchema(instance.searchIndex)); await deployer.rebuildIndex(instance, new AzureDeployerService(deployer).getKBSearchSchema(instance.searchIndex));
} }
/** /**
* Deploys a knowledge base to the storage using the .gbkb format. * Deploys a knowledge base to the storage using the .gbkb format.
@ -480,17 +559,17 @@ private async playVideo(conversationalService: IGBConversationalService, step: G
* @param localPath Path to the .gbkb folder. * @param localPath Path to the .gbkb folder.
*/ */
public async deployKb(core: IGBCoreService, deployer: GBDeployer, localPath: string) { public async deployKb(core: IGBCoreService, deployer: GBDeployer, localPath: string) {
const packageType = Path.extname(localPath); const packageType = Path.extname(localPath);
const packageName = Path.basename(localPath); const packageName = Path.basename(localPath);
GBLog.info(`[GBDeployer] Opening package: ${localPath}`); GBLog.info(`[GBDeployer] Opening package: ${localPath}`);
const packageObject = JSON.parse(Fs.readFileSync(urlJoin(localPath, 'package.json'), 'utf8')); const packageObject = JSON.parse(Fs.readFileSync(urlJoin(localPath, 'package.json'), 'utf8'));
const instance = await core.loadInstance(packageObject.botId); const instance = await core.loadInstance(packageObject.botId);
GBLog.info(`[GBDeployer] Importing: ${localPath}`); GBLog.info(`[GBDeployer] Importing: ${localPath}`);
const p = await deployer.deployPackageToStorage(instance.instanceId, packageName); const p = await deployer.deployPackageToStorage(instance.instanceId, packageName);
await this.importKbPackage(localPath, p, instance); await this.importKbPackage(localPath, p, instance);
deployer.rebuildIndex(instance, new AzureDeployerService(deployer).getKBSearchSchema(instance.searchIndex)); deployer.rebuildIndex(instance, new AzureDeployerService(deployer).getKBSearchSchema(instance.searchIndex));
GBLog.info(`[GBDeployer] Finished import of ${localPath}`); GBLog.info(`[GBDeployer] Finished import of ${localPath}`);
} }
} }

View file

@ -231,14 +231,15 @@ export class WhatsappDirectLine extends GBService {
return `${attachment.content.title} - ${attachment.content.text}`; return `${attachment.content.title} - ${attachment.content.text}`;
} }
public async sendFileToDevice(to, url) { public async sendFileToDevice(to, url, filename) {
const options = { const options = {
method: 'POST', method: 'POST',
url: urlJoin(this.whatsappServiceUrl, 'sendFile'), url: urlJoin(this.whatsappServiceUrl, 'sendFile'),
qs: { qs: {
token: this.whatsappServiceKey, token: this.whatsappServiceKey,
phone: to, phone: to,
body: url body: url,
filename: filename
}, },
headers: { headers: {
'cache-control': 'no-cache' 'cache-control': 'no-cache'
@ -254,6 +255,28 @@ export class WhatsappDirectLine extends GBService {
} }
} }
public async sendAudioToDevice(to, url) {
const options = {
method: 'POST',
url: urlJoin(this.whatsappServiceUrl, 'sendPTT'),
qs: {
token: this.whatsappServiceKey,
phone: to,
audio:url
},
headers: {
'cache-control': 'no-cache'
}
};
try {
// tslint:disable-next-line: await-promise
const result = await request.post(options);
GBLog.info(result);
} catch (error) {
GBLog.error(`Error sending message to Whatsapp provider ${error.message}`);
}
}
public async sendToDevice(to, msg) { public async sendToDevice(to, msg) {
const options = { const options = {

View file

@ -39,7 +39,7 @@
const express = require('express'); const express = require('express');
const bodyParser = require('body-parser'); const bodyParser = require('body-parser');
import { GBLog, IGBCoreService, IGBInstance, IGBPackage } from 'botlib'; import { GBLog, IGBCoreService, IGBInstance, IGBPackage, GBMinInstance } from 'botlib';
import { GBAdminService } from '../packages/admin.gbapp/services/GBAdminService'; import { GBAdminService } from '../packages/admin.gbapp/services/GBAdminService';
import { AzureDeployerService } from '../packages/azuredeployer.gbapp/services/AzureDeployerService'; import { AzureDeployerService } from '../packages/azuredeployer.gbapp/services/AzureDeployerService';
import { GBConfigService } from '../packages/core.gbapp/services/GBConfigService'; import { GBConfigService } from '../packages/core.gbapp/services/GBConfigService';
@ -60,6 +60,7 @@ export class RootData {
minService: GBMinService; minService: GBMinService;
bootInstance: IGBInstance; bootInstance: IGBInstance;
public minInstances: any[]; public minInstances: any[];
minBoot: GBMinInstance;
} }
/** /**