diff --git a/package-lock.json b/package-lock.json index 01f22d33..da77ce88 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4953,6 +4953,15 @@ "resolved": "https://registry.npmjs.org/babyparse/-/babyparse-0.2.1.tgz", "integrity": "sha1-Bp8DXfP9zm86RV3V2vx1F43PN2A=" }, + "backbone": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/backbone/-/backbone-1.4.1.tgz", + "integrity": "sha512-ADy1ztN074YkWbHi8ojJVFe3vAanO/lrzMGZWUClIP7oDD/Pjy2vrASraUP+2EVCfIiTtCW4FChVow01XneivA==", + "dev": true, + "requires": { + "underscore": ">=1.8.3" + } + }, "bail": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/bail/-/bail-1.0.5.tgz", @@ -11106,6 +11115,12 @@ } } }, + "highlight.js": { + "version": "9.18.5", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-9.18.5.tgz", + "integrity": "sha512-a5bFyofd/BHCX52/8i8uJkjr9DYwXIPnM/plwI6W7ezItLGqzt7X2G2nXuYSfsIJdkwwj/g9DG1LkcGJI/dDoA==", + "dev": true + }, "hoek": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/hoek/-/hoek-4.2.1.tgz", @@ -12295,6 +12310,12 @@ "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-26.0.0.tgz", "integrity": "sha512-Gv3ZIs/nA48/Zvjrl34bf+oD76JHiGDUxNOVgUjh3j890sblXryjY4rss71fPtD/njchl6PSE2hIhvyWa1eT0A==" }, + "jquery": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.6.0.tgz", + "integrity": "sha512-JVzAR/AjBvVt2BmYhxRCSYysDsPcssdmTFnzyLEts9qNwmjmu4JTAMYubEfwVOSwpQ1I1sKKFcxhZCI2buerfw==", + "dev": true + }, "js-base64": { "version": "2.6.3", "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.6.3.tgz", @@ -16713,32 +16734,6 @@ "mimic-fn": "^2.1.0" } }, - "onigasm": { - "version": "2.2.5", - "resolved": "https://registry.npmjs.org/onigasm/-/onigasm-2.2.5.tgz", - "integrity": "sha512-F+th54mPc0l1lp1ZcFMyL/jTs2Tlq4SqIHKIXGZOR/VkHkF9A7Fr5rRr5+ZG/lWeRsyrClLYRq7s/yFQ/XhWCA==", - "dev": true, - "requires": { - "lru-cache": "^5.1.1" - }, - "dependencies": { - "lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "dev": true, - "requires": { - "yallist": "^3.0.2" - } - }, - "yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "dev": true - } - } - }, "open": { "version": "7.4.2", "resolved": "https://registry.npmjs.org/open/-/open-7.4.2.tgz", @@ -19785,9 +19780,9 @@ "dev": true }, "shelljs": { - "version": "0.8.4", - "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.8.4.tgz", - "integrity": "sha512-7gk3UZ9kOfPLIAbslLzyWeGiEqx9e3rxwZM0KE6EL8GlGwjym9Mrlx5/p33bWTu9YG6vcS4MBxYZDHYr5lr8BQ==", + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.8.5.tgz", + "integrity": "sha512-TiwcRcrkhHvbrZbnRcFYMLl30Dfov3HKqzp5tO5b4pt6G/SezKcYhmDg15zXVBswHmctSAQKznqNW2LO5tTDow==", "dev": true, "requires": { "glob": "^7.0.0", @@ -19795,16 +19790,6 @@ "rechoir": "^0.6.2" } }, - "shiki": { - "version": "0.9.3", - "resolved": "https://registry.npmjs.org/shiki/-/shiki-0.9.3.tgz", - "integrity": "sha512-NEjg1mVbAUrzRv2eIcUt3TG7X9svX7l3n3F5/3OdFq+/BxUdmBOeKGiH4icZJBLHy354Shnj6sfBTemea2e7XA==", - "dev": true, - "requires": { - "onigasm": "^2.2.5", - "vscode-textmate": "^5.2.0" - } - }, "side-channel": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", @@ -21793,90 +21778,60 @@ } }, "typedoc": { - "version": "0.20.36", - "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.20.36.tgz", - "integrity": "sha512-qFU+DWMV/hifQ9ZAlTjdFO9wbUIHuUBpNXzv68ZyURAP9pInjZiO4+jCPeAzHVcaBCHER9WL/+YzzTt6ZlN/Nw==", + "version": "0.16.11", + "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.16.11.tgz", + "integrity": "sha512-YEa5i0/n0yYmLJISJ5+po6seYfJQJ5lQYcHCPF9ffTF92DB/TAZO/QrazX5skPHNPtmlIht5FdTXCM2kC7jQFQ==", "dev": true, "requires": { - "colors": "^1.4.0", - "fs-extra": "^9.1.0", - "handlebars": "^4.7.7", - "lodash": "^4.17.21", - "lunr": "^2.3.9", - "marked": "^2.0.3", + "@types/minimatch": "3.0.3", + "fs-extra": "^8.1.0", + "handlebars": "^4.7.2", + "highlight.js": "^9.17.1", + "lodash": "^4.17.15", + "marked": "^0.8.0", "minimatch": "^3.0.0", "progress": "^2.0.3", - "shelljs": "^0.8.4", - "shiki": "^0.9.3", - "typedoc-default-themes": "^0.12.10" + "shelljs": "^0.8.3", + "typedoc-default-themes": "^0.7.2", + "typescript": "3.7.x" }, "dependencies": { "fs-extra": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", - "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", "dev": true, "requires": { - "at-least-node": "^1.0.0", "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" } }, - "handlebars": { - "version": "4.7.7", - "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.7.tgz", - "integrity": "sha512-aAcXm5OAfE/8IXkcZvCepKU3VzW1/39Fb5ZuqMtgI/hT8X2YgoMvBY5dLhq/cpOvw7Lk1nK/UF71aLG/ZnVYRA==", - "dev": true, - "requires": { - "minimist": "^1.2.5", - "neo-async": "^2.6.0", - "source-map": "^0.6.1", - "uglify-js": "^3.1.4", - "wordwrap": "^1.0.0" - } - }, - "jsonfile": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", - "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", - "dev": true, - "requires": { - "graceful-fs": "^4.1.6", - "universalify": "^2.0.0" - } - }, - "lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "marked": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/marked/-/marked-0.8.2.tgz", + "integrity": "sha512-EGwzEeCcLniFX51DhTpmTom+dSA/MG/OBUDjnWtHbEnjAH180VzUeAw+oE4+Zv+CoYBWyRlYOTR0N8SO9R1PVw==", "dev": true }, - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - }, - "universalify": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", - "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", - "dev": true - }, - "wordwrap": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", - "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=", + "typescript": { + "version": "3.7.7", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.7.7.tgz", + "integrity": "sha512-MmQdgo/XenfZPvVLtKZOq9jQQvzaUAUpcKW8Z43x9B2fOm4S5g//tPtMweZUIP+SoBqrVPEIm+dJeQ9dfO0QdA==", "dev": true } } }, "typedoc-default-themes": { - "version": "0.12.10", - "resolved": "https://registry.npmjs.org/typedoc-default-themes/-/typedoc-default-themes-0.12.10.tgz", - "integrity": "sha512-fIS001cAYHkyQPidWXmHuhs8usjP5XVJjWB8oZGqkTowZaz3v7g3KDZeeqE82FBrmkAnIBOY3jgy7lnPnqATbA==", - "dev": true + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/typedoc-default-themes/-/typedoc-default-themes-0.7.2.tgz", + "integrity": "sha512-fiFKlFO6VTqjcno8w6WpTsbCgXmfPHVjnLfYkmByZE7moaz+E2DSpAT+oHtDHv7E0BM5kAhPrHJELP2J2Y2T9A==", + "dev": true, + "requires": { + "backbone": "^1.4.0", + "jquery": "^3.4.1", + "lunr": "^2.3.8", + "underscore": "^1.9.1" + } }, "typescript": { "version": "3.6.4", @@ -22361,12 +22316,6 @@ } } }, - "vscode-textmate": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/vscode-textmate/-/vscode-textmate-5.4.0.tgz", - "integrity": "sha512-c0Q4zYZkcLizeYJ3hNyaVUM2AA8KDhNCA3JvXY8CeZSJuBdAy3bAvSbv46RClC4P3dSO9BdwhnKEx2zOo6vP/w==", - "dev": true - }, "walk-promise": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/walk-promise/-/walk-promise-0.2.0.tgz", diff --git a/packages/core.gbapp/services/GBMinService.ts b/packages/core.gbapp/services/GBMinService.ts index 2f0bfec9..6baef3d4 100644 --- a/packages/core.gbapp/services/GBMinService.ts +++ b/packages/core.gbapp/services/GBMinService.ts @@ -34,1348 +34,1352 @@ * @fileoverview General Bots server core. */ -'use strict'; -const { DialogSet, TextPrompt } = require('botbuilder-dialogs'); -const express = require('express'); -const Fs = require('fs'); -const request = require('request-promise-native'); -const removeRoute = require('express-remove-route'); -const ssrForBots = require("ssr-for-bots").default; -const AuthenticationContext = require('adal-node').AuthenticationContext; -const wash = require('washyourmouthoutwithsoap'); -const { FacebookAdapter } = require('botbuilder-adapter-facebook'); -const path = require('path'); -import { - AutoSaveStateMiddleware, - BotFrameworkAdapter, - ConversationState, - MemoryStorage, - TurnContext, - UserState -} from 'botbuilder'; -import { ConfirmPrompt, OAuthPrompt, WaterfallDialog } from 'botbuilder-dialogs'; -import { - GBDialogStep, - GBLog, - GBMinInstance, - IGBAdminService, - IGBConversationalService, - IGBCoreService, - IGBInstance, - IGBPackage -} from 'botlib'; -import { CollectionUtil } from 'pragmatismo-io-framework'; -import { MicrosoftAppCredentials } from 'botframework-connector'; -import { GBServer } from '../../../src/app'; -import { GBAdminService } from '../../admin.gbapp/services/GBAdminService'; -import { GuaribasConversationMessage } from '../../analytics.gblib/models'; -import { AnalyticsService } from '../../analytics.gblib/services/AnalyticsService'; -import { GBVMService } from '../../basic.gblib/services/GBVMService'; -import { AskDialogArgs } from '../../kb.gbapp/dialogs/AskDialog'; -import { KBService } from '../../kb.gbapp/services/KBService'; -import { SecService } from '../../security.gbapp/services/SecService'; -import { WhatsappDirectLine } from '../../whatsapp.gblib/services/WhatsappDirectLine'; -import { Messages } from '../strings'; -import { GBConfigService } from './GBConfigService'; -import { GBConversationalService } from './GBConversationalService'; -import { GBDeployer } from './GBDeployer'; -import urlJoin = require('url-join'); -import fs = require('fs'); -import { GoogleChatDirectLine } from '../../google-chat.gblib/services/GoogleChatDirectLine'; -import { ScheduleServices } from '../../basic.gblib/services/ScheduleServices'; -import { SystemKeywords } from '../../basic.gblib/services/SystemKeywords'; - -/** - * Minimal service layer for a bot and encapsulation of BOT Framework calls. - */ -export class GBMinService { - - /** - * Default General Bots User Interface package. - */ - private static uiPackage = 'default.gbui'; - - /** - * Main core service attached to this bot service. - */ - public core: IGBCoreService; - - /** - * Reference to conversation services like receive and prompt text. - */ - public conversationalService: IGBConversationalService; - - /** - * Conversational administration services like publishing packages. - */ - public adminService: IGBAdminService; - - /** - * Deployent of packages and publishing related services. - */ - public deployer: GBDeployer; - - /** - * Static initialization of minimal instance. - */ - constructor( - core: IGBCoreService, - conversationalService: IGBConversationalService, - adminService: IGBAdminService, - deployer: GBDeployer - ) { - this.core = core; - this.conversationalService = conversationalService; - this.adminService = adminService; - this.deployer = deployer; - } - - /** - * Constructs a new minimal instance for each bot. - */ - public async buildMin(instances: IGBInstance[]) { - - // Servers default UI on root address '/' if web enabled. - - if (process.env.DISABLE_WEB !== 'true') { - - // SSR processing. - - const defaultOptions = { - prerender: [], - exclude: ["/api/", "/instances/", "/webhooks/"], - useCache: true, - cacheRefreshRate: 86400 - }; - GBServer.globals.server.use(ssrForBots(defaultOptions)); - - const url = GBServer.globals.wwwroot - ? GBServer.globals.wwwroot - : urlJoin(GBDeployer.deployFolder, GBMinService.uiPackage, 'build'); - - // default.gbui access definition. - - GBServer.globals.server.use('/', express.static(url)); - GBServer.globals.server.use('/ssr-delay', async (req,res) => { - - const sleep = async ms => { - return new Promise(resolve => { - setTimeout(resolve, ms); - }); - }; - await sleep(10000); - res.status(200); - res.end(); - }); - } - - // Servers the bot information object via HTTP so clients can get - // instance information stored on server. - - if (process.env.DISABLE_WEB !== 'true') { - GBServer.globals.server.get('/instances/:botId', this.handleGetInstanceForClient.bind(this)); - } - - // Servers the WhatsApp callback. - - GBServer.globals.server.post('/webhooks/whatsapp/:botId', this.WhatsAppCallback.bind(this)); - - // Calls mountBot event to all bots. - - await CollectionUtil.asyncForEach(instances, async instance => { - try { - await this.mountBot(instance); - } catch (error) { - GBLog.error(`Error mounting bot ${instance.botId}: ${error.message}\n${error.stack}`); - } - }); - - // Then all remaining general packages are loaded. - - await CollectionUtil.asyncForEach(instances, async instance => { - GBDeployer.mountGBKBAssets(`${instance.botId}.gbkb`, - instance.botId, `${instance.botId}.gbkb`); - }); - - GBLog.info(`Package deployment done.`); - - - } - - - - /** - * Removes bot endpoint from web listeners and remove bot instance - * from list of global server bot instances. - */ - public async unmountBot(botId: string) { - const url = `/api/messages/${botId}`; - removeRoute(GBServer.globals.server, url); - - const uiUrl = `/${botId}`; - removeRoute(GBServer.globals.server, uiUrl); - - GBServer.globals.minInstances = GBServer.globals.minInstances.filter(p => p.instance.botId !== botId); - - } - - /** - * Mount the instance by creating an BOT Framework bot object, - * serving bot endpoint in several URL like WhatsApp endpoint, .gbkb assets, - * installing all BASIC artifacts from .gbdialog and OAuth2. - */ - public async mountBot(instance: IGBInstance) { - - // Build bot adapter. - - const { min, adapter, conversationState } = await this.buildBotAdapter( - instance, - GBServer.globals.sysPackages, - GBServer.globals.appPackages - ); - min['groupCache'] = await KBService.getGroupReplies(instance.instanceId); - GBServer.globals.minInstances.push(min); - - await this.deployer.deployPackage(min, 'packages/default.gbtheme'); - - // Install per bot deployed packages. - - let packagePath = `work/${min.botId}.gbai/${min.botId}.gbdialog`; - if (fs.existsSync(packagePath)) { - await this.deployer.deployPackage(min, packagePath); - } - packagePath = `work/${min.botId}.gbai/${min.botId}.gbapp`; - if (fs.existsSync(packagePath)) { - await this.deployer.deployPackage(min, packagePath); - } - packagePath = `work/${min.botId}.gbai/${min.botId}.gbtheme`; - if (fs.existsSync(packagePath)) { - await this.deployer.deployPackage(min, packagePath); - } - packagePath = `work/${min.botId}.gbai/${min.botId}.gblib`; - if (fs.existsSync(packagePath)) { - await this.deployer.deployPackage(min, packagePath); - } - - const service = new ScheduleServices(); - await service.loadSchedules(min); - - // Calls the loadBot context.activity for all packages. - - await this.invokeLoadBot(GBServer.globals.appPackages, GBServer.globals.sysPackages, min); - - // Serves individual URL for each bot conversational interface. - - const receiver = async (req, res) => { - await this.receiver(req, res, conversationState, min, instance, GBServer.globals.appPackages); - }; - const url = `/api/messages/${instance.botId}`; - GBServer.globals.server.post(url, receiver); - GBServer.globals.server.get(url, (req, res) => { - if (req.query['hub.mode'] === 'subscribe') { - if (req.query['hub.verify_token'] === process.env.FACEBOOK_VERIFY_TOKEN) { - const val = req.query['hub.challenge']; - res.send(val); - } else { - GBLog.error('Failed to verify endpoint.'); - res.send('OK'); - } - } - res.end(); - }); - GBLog.info(`GeneralBots(${instance.engineName}) listening on: ${url}.`); - - // Serves individual URL for each bot user interface. - - if (process.env.DISABLE_WEB !== 'true') { - const uiUrl = `/${instance.botId}`; - const uiUrlAlt = `/${instance.activationCode}`; - GBServer.globals.server.use( - uiUrl, - express.static(urlJoin(GBDeployer.deployFolder, GBMinService.uiPackage, 'build')) - ); - GBServer.globals.server.use( - uiUrlAlt, - express.static(urlJoin(GBDeployer.deployFolder, GBMinService.uiPackage, 'build')) - ); - const domain = min.core.getParam(min.instance, 'Domain', null); - if (domain) { - GBServer.globals.server.use( - domain, - express.static(urlJoin(GBDeployer.deployFolder, GBMinService.uiPackage, 'build')) - ); - GBLog.info(`Bot UI ${GBMinService.uiPackage} accessible at custom domain: ${domain}.`); - } - GBLog.info(`Bot UI ${GBMinService.uiPackage} accessible at: ${uiUrl} and ${uiUrlAlt}.`); - } - - // Clients get redirected here in order to create an OAuth authorize url and redirect them to AAD. - // There they will authenticate and give their consent to allow this app access to - // some resource they own. - - this.handleOAuthRequests(GBServer.globals.server, min); - - // After consent is granted AAD redirects here. The ADAL library - // is invoked via the AuthenticationContext and retrieves an - // access token that can be used to access the user owned resource. - - this.handleOAuthTokenRequests(GBServer.globals.server, min, instance); - - // Provides checking of instance health. - - this.createCheckHealthAddress(GBServer.globals.server, min, min.instance); - } - - private async WhatsAppCallback(req, res) { - try { - - // Detects if the message is echo from itself. - - const id = req.body.messages[0].author.split('@')[0]; - const senderName = req.body.messages[0].senderName; - const sec = new SecService(); - let user = await sec.getUserFromSystemId(id); - - if (req.body.messages[0].fromMe) { - res.end(); - - return; // Exit here. - } - - let activeMin; - let botId = req.params.botId; - if (botId === '[default]' || botId === undefined) { - botId = GBConfigService.get('BOT_ID'); - } - - GBLog.info(`Client requested instance for: ${botId}.`); - - - // Processes group behaviour. - - let text = req.body.messages[0].body; - text = text.replace(/\@\d+ /gi, ''); - - // Ensures that the bot group is the active bot for the user (like switching). - - const message = req.body.messages[0]; - if (message.chatName.charAt(0) !== '+') { - const group = message.chatName; - - const botGroup = await this.core.loadInstanceByBotId(group); - if (user.instanceId !== botGroup.instanceId) { - await sec.updateUserInstance(id, botGroup.instanceId); - } - activeMin = GBServer.globals.minInstances.filter - (p => p.instance.instanceId === botGroup.instanceId)[0]; - await (activeMin as any).whatsAppDirectLine.received(req, res); - return; // EXIT HERE. - } - - - // Detects if the welcome message is enabled. - - if (process.env.WHATSAPP_WELCOME_DISABLED !== 'true') { - - // Tries to find if user wants to switch bots. - - let toSwitchMin = GBServer.globals.minInstances.filter( - p => p.instance.botId.toLowerCase() === text.toLowerCase() - )[0]; - if (!toSwitchMin) { - toSwitchMin = GBServer.globals.minInstances.filter(p => - p.instance.activationCode ? p.instance.activationCode.toLowerCase() === text.toLowerCase() : false - )[0]; - } - - // Find active bot instance. - - activeMin = toSwitchMin ? toSwitchMin : GBServer.globals.minBoot; - - // If it is the first time for the user, tries to auto-execute - // start dialog if any is specified in Config.xlsx. - - if (user === null || user.hearOnDialog) { - user = await sec.ensureUser(activeMin.instance.instanceId, id, senderName, '', 'whatsapp', senderName, null); - - const startDialog = user.hearOnDialog ? - user.hearOnDialog : - activeMin.core.getParam(activeMin.instance, 'Start Dialog', null); - - if (startDialog) { - GBLog.info(`Calling /start to Auto start ${startDialog} for ${activeMin.instance.instanceId}...`); - req.body.messages[0].body = `/start`; - - // Resets HEAR ON DIALOG value to none and passes - // current dialog to the direct line. - - await sec.updateUserHearOnDialog(user.userId, null); - await (activeMin as any).whatsAppDirectLine.received(req, res); - } else { - await (activeMin as any).whatsAppDirectLine.sendToDevice( - id, - `Olá! Seja bem-vinda(o)!\nMe chamo ${activeMin.instance.title}. Como posso ajudar? Pode me falar que eu te ouço, me manda um aúdio.` - , null); - res.end(); + 'use strict'; + const { DialogSet, TextPrompt } = require('botbuilder-dialogs'); + const express = require('express'); + const Fs = require('fs'); + const request = require('request-promise-native'); + const removeRoute = require('express-remove-route'); + const ssrForBots = require("ssr-for-bots").default; + const AuthenticationContext = require('adal-node').AuthenticationContext; + const wash = require('washyourmouthoutwithsoap'); + const { FacebookAdapter } = require('botbuilder-adapter-facebook'); + const path = require('path'); + import { + AutoSaveStateMiddleware, + BotFrameworkAdapter, + ConversationState, + MemoryStorage, + TurnContext, + UserState + } from 'botbuilder'; + import { ConfirmPrompt, OAuthPrompt, WaterfallDialog } from 'botbuilder-dialogs'; + import { + GBDialogStep, + GBLog, + GBMinInstance, + IGBAdminService, + IGBConversationalService, + IGBCoreService, + IGBInstance, + IGBPackage + } from 'botlib'; + import { CollectionUtil } from 'pragmatismo-io-framework'; + import { MicrosoftAppCredentials } from 'botframework-connector'; + import { GBServer } from '../../../src/app'; + import { GBAdminService } from '../../admin.gbapp/services/GBAdminService'; + import { GuaribasConversationMessage } from '../../analytics.gblib/models'; + import { AnalyticsService } from '../../analytics.gblib/services/AnalyticsService'; + import { GBVMService } from '../../basic.gblib/services/GBVMService'; + import { AskDialogArgs } from '../../kb.gbapp/dialogs/AskDialog'; + import { KBService } from '../../kb.gbapp/services/KBService'; + import { SecService } from '../../security.gbapp/services/SecService'; + import { WhatsappDirectLine } from '../../whatsapp.gblib/services/WhatsappDirectLine'; + import { Messages } from '../strings'; + import { GBConfigService } from './GBConfigService'; + import { GBConversationalService } from './GBConversationalService'; + import { GBDeployer } from './GBDeployer'; + import urlJoin = require('url-join'); + import fs = require('fs'); + import { GoogleChatDirectLine } from '../../google-chat.gblib/services/GoogleChatDirectLine'; + import { ScheduleServices } from '../../basic.gblib/services/ScheduleServices'; + import { SystemKeywords } from '../../basic.gblib/services/SystemKeywords'; + + /** + * Minimal service layer for a bot and encapsulation of BOT Framework calls. + */ + export class GBMinService { + + /** + * Default General Bots User Interface package. + */ + private static uiPackage = 'default.gbui'; + + /** + * Main core service attached to this bot service. + */ + public core: IGBCoreService; + + /** + * Reference to conversation services like receive and prompt text. + */ + public conversationalService: IGBConversationalService; + + /** + * Conversational administration services like publishing packages. + */ + public adminService: IGBAdminService; + + /** + * Deployent of packages and publishing related services. + */ + public deployer: GBDeployer; + + /** + * Static initialization of minimal instance. + */ + constructor( + core: IGBCoreService, + conversationalService: IGBConversationalService, + adminService: IGBAdminService, + deployer: GBDeployer + ) { + this.core = core; + this.conversationalService = conversationalService; + this.adminService = adminService; + this.deployer = deployer; + } + + /** + * Constructs a new minimal instance for each bot. + */ + public async buildMin(instances: IGBInstance[]) { + + // Servers default UI on root address '/' if web enabled. + + if (process.env.DISABLE_WEB !== 'true') { + + // SSR processing. + + const defaultOptions = { + prerender: [], + exclude: ["/api/", "/instances/", "/webhooks/"], + useCache: true, + cacheRefreshRate: 86400 + }; + GBServer.globals.server.use(ssrForBots(defaultOptions)); + + const url = GBServer.globals.wwwroot + ? GBServer.globals.wwwroot + : urlJoin(GBDeployer.deployFolder, GBMinService.uiPackage, 'build'); + + // default.gbui access definition. + + GBServer.globals.server.use('/', express.static(url)); + GBServer.globals.server.use('/ssr-delay', async (req,res) => { + + const sleep = async ms => { + return new Promise(resolve => { + setTimeout(resolve, ms); + }); + }; + await sleep(20000); + res.status(200); + res.end(); + }); + } + + // Servers the bot information object via HTTP so clients can get + // instance information stored on server. + + if (process.env.DISABLE_WEB !== 'true') { + GBServer.globals.server.get('/instances/:botId', this.handleGetInstanceForClient.bind(this)); + } + + // Servers the WhatsApp callback. + + GBServer.globals.server.post('/webhooks/whatsapp/:botId', this.WhatsAppCallback.bind(this)); + + // Calls mountBot event to all bots. + + await CollectionUtil.asyncForEach(instances, async instance => { + try { + await this.mountBot(instance); + } catch (error) { + GBLog.error(`Error mounting bot ${instance.botId}: ${error.message}\n${error.stack}`); + } + }); + + // Then all remaining general packages are loaded. + + await CollectionUtil.asyncForEach(instances, async instance => { + GBDeployer.mountGBKBAssets(`${instance.botId}.gbkb`, + instance.botId, `${instance.botId}.gbkb`); + }); + + GBLog.info(`Package deployment done.`); + + + } + + + + /** + * Removes bot endpoint from web listeners and remove bot instance + * from list of global server bot instances. + */ + public async unmountBot(botId: string) { + const url = `/api/messages/${botId}`; + removeRoute(GBServer.globals.server, url); + + const uiUrl = `/${botId}`; + removeRoute(GBServer.globals.server, uiUrl); + + GBServer.globals.minInstances = GBServer.globals.minInstances.filter(p => p.instance.botId !== botId); + + } + + /** + * Mount the instance by creating an BOT Framework bot object, + * serving bot endpoint in several URL like WhatsApp endpoint, .gbkb assets, + * installing all BASIC artifacts from .gbdialog and OAuth2. + */ + public async mountBot(instance: IGBInstance) { + + // Build bot adapter. + + const { min, adapter, conversationState } = await this.buildBotAdapter( + instance, + GBServer.globals.sysPackages, + GBServer.globals.appPackages + ); + min['groupCache'] = await KBService.getGroupReplies(instance.instanceId); + GBServer.globals.minInstances.push(min); + + await this.deployer.deployPackage(min, 'packages/default.gbtheme'); + + // Install per bot deployed packages. + + let packagePath = `work/${min.botId}.gbai/${min.botId}.gbdialog`; + if (fs.existsSync(packagePath)) { + await this.deployer.deployPackage(min, packagePath); + } + packagePath = `work/${min.botId}.gbai/${min.botId}.gbapp`; + if (fs.existsSync(packagePath)) { + await this.deployer.deployPackage(min, packagePath); + } + packagePath = `work/${min.botId}.gbai/${min.botId}.gbtheme`; + if (fs.existsSync(packagePath)) { + await this.deployer.deployPackage(min, packagePath); + } + packagePath = `work/${min.botId}.gbai/${min.botId}.gblib`; + if (fs.existsSync(packagePath)) { + await this.deployer.deployPackage(min, packagePath); + } + + const service = new ScheduleServices(); + await service.loadSchedules(min); + + // Calls the loadBot context.activity for all packages. + + await this.invokeLoadBot(GBServer.globals.appPackages, GBServer.globals.sysPackages, min); + + // Serves individual URL for each bot conversational interface. + + const receiver = async (req, res) => { + await this.receiver(req, res, conversationState, min, instance, GBServer.globals.appPackages); + }; + const url = `/api/messages/${instance.botId}`; + GBServer.globals.server.post(url, receiver); + GBServer.globals.server.get(url, (req, res) => { + if (req.query['hub.mode'] === 'subscribe') { + if (req.query['hub.verify_token'] === process.env.FACEBOOK_VERIFY_TOKEN) { + const val = req.query['hub.challenge']; + res.send(val); + } else { + GBLog.error('Failed to verify endpoint.'); + res.send('OK'); + } + } + res.end(); + }); + GBLog.info(`GeneralBots(${instance.engineName}) listening on: ${url}.`); + + // Serves individual URL for each bot user interface. + + if (process.env.DISABLE_WEB !== 'true') { + const uiUrl = `/${instance.botId}`; + const uiUrlAlt = `/${instance.activationCode}`; + GBServer.globals.server.use( + uiUrl, + express.static(urlJoin(GBDeployer.deployFolder, GBMinService.uiPackage, 'build')) + ); + GBServer.globals.server.use( + uiUrlAlt, + express.static(urlJoin(GBDeployer.deployFolder, GBMinService.uiPackage, 'build')) + ); + const domain = min.core.getParam(min.instance, 'Domain', null); + if (domain) { + GBServer.globals.server.use( + domain, + express.static(urlJoin(GBDeployer.deployFolder, GBMinService.uiPackage, 'build')) + ); + GBLog.info(`Bot UI ${GBMinService.uiPackage} accessible at custom domain: ${domain}.`); + } + GBLog.info(`Bot UI ${GBMinService.uiPackage} accessible at: ${uiUrl} and ${uiUrlAlt}.`); + } + + // Clients get redirected here in order to create an OAuth authorize url and redirect them to AAD. + // There they will authenticate and give their consent to allow this app access to + // some resource they own. + + this.handleOAuthRequests(GBServer.globals.server, min); + + // After consent is granted AAD redirects here. The ADAL library + // is invoked via the AuthenticationContext and retrieves an + // access token that can be used to access the user owned resource. + + this.handleOAuthTokenRequests(GBServer.globals.server, min, instance); + + // Provides checking of instance health. + + this.createCheckHealthAddress(GBServer.globals.server, min, min.instance); + } + + private async WhatsAppCallback(req, res) { + try { + + // Detects if the message is echo from itself. + + const id = req.body.messages[0].author.split('@')[0]; + const senderName = req.body.messages[0].senderName; + const sec = new SecService(); + let user = await sec.getUserFromSystemId(id); + + if (req.body.messages[0].fromMe) { + res.end(); + + return; // Exit here. + } + + let activeMin; + let botId = req.params.botId; + if (botId === '[default]' || botId === undefined) { + botId = GBConfigService.get('BOT_ID'); + } + + GBLog.info(`Client requested instance for: ${botId}.`); + + + // Processes group behaviour. + + let text = req.body.messages[0].body; + text = text.replace(/\@\d+ /gi, ''); + + // Ensures that the bot group is the active bot for the user (like switching). + + const message = req.body.messages[0]; + if (message.chatName.charAt(0) !== '+') { + const group = message.chatName; + + const botGroup = await this.core.loadInstanceByBotId(group); + if (user.instanceId !== botGroup.instanceId) { + await sec.updateUserInstance(id, botGroup.instanceId); + } + activeMin = GBServer.globals.minInstances.filter + (p => p.instance.instanceId === botGroup.instanceId)[0]; + await (activeMin as any).whatsAppDirectLine.received(req, res); + return; // EXIT HERE. + } + + + // Detects if the welcome message is enabled. + + if (process.env.WHATSAPP_WELCOME_DISABLED !== 'true') { + + // Tries to find if user wants to switch bots. + + let toSwitchMin = GBServer.globals.minInstances.filter( + p => p.instance.botId.toLowerCase() === text.toLowerCase() + )[0]; + if (!toSwitchMin) { + toSwitchMin = GBServer.globals.minInstances.filter(p => + p.instance.activationCode ? p.instance.activationCode.toLowerCase() === text.toLowerCase() : false + )[0]; + } + + // Find active bot instance. + + activeMin = toSwitchMin ? toSwitchMin : GBServer.globals.minBoot; + + // If it is the first time for the user, tries to auto-execute + // start dialog if any is specified in Config.xlsx. + + if (user === null || user.hearOnDialog) { + user = await sec.ensureUser(activeMin.instance.instanceId, id, senderName, '', 'whatsapp', senderName, null); + + const startDialog = user.hearOnDialog ? + user.hearOnDialog : + activeMin.core.getParam(activeMin.instance, 'Start Dialog', null); + + if (startDialog) { + GBLog.info(`Calling /start to Auto start ${startDialog} for ${activeMin.instance.instanceId}...`); + req.body.messages[0].body = `/start`; + + // Resets HEAR ON DIALOG value to none and passes + // current dialog to the direct line. + + await sec.updateUserHearOnDialog(user.userId, null); + await (activeMin as any).whatsAppDirectLine.received(req, res); + } else { + await (activeMin as any).whatsAppDirectLine.sendToDevice( + id, + `Olá! Seja bem-vinda(o)!\nMe chamo ${activeMin.instance.title}. Como posso ajudar? Pode me falar que eu te ouço, me manda um aúdio.` + , null); + res.end(); + } + } else { + + // User wants to switch bots. + + if (toSwitchMin !== undefined) { + + // So gets the new bot instance information and prepares to + // auto start dialog if any is specified. + + const instance = await this.core.loadInstanceByBotId(activeMin.botId); + await sec.updateUserInstance(id, instance.instanceId); + await (activeMin as any).whatsAppDirectLine.resetConversationId(id, ''); + const startDialog = activeMin.core.getParam(activeMin.instance, 'Start Dialog', null); + + + if (startDialog) { + GBLog.info(`Calling /start for Auto start : ${startDialog} for ${activeMin.instance.botId}...`); + req.body.messages[0].body = `/start`; + await (activeMin as any).whatsAppDirectLine.received(req, res); + } else { + await (activeMin as any).whatsAppDirectLine.sendToDevice( + id, + `Agora falando com ${activeMin.instance.title}...`, + null + ); + + + } + res.end(); + } else { + activeMin = GBServer.globals.minInstances.filter(p => p.instance.instanceId === user.instanceId)[0]; + if (activeMin === undefined) { + activeMin = GBServer.globals.minBoot; + await (activeMin as any).whatsAppDirectLine.sendToDevice( + id, + `O outro Bot que você estava falando(${user.instanceId}), não está mais disponível. Agora você está falando comigo, ${activeMin.instance.title}...` + ); + } + await (activeMin as any).whatsAppDirectLine.received(req, res); + } + } + } else { + let minInstance = GBServer.globals.minInstances.filter( + p => p.instance.botId.toLowerCase() === botId + )[0]; + + + // Just pass the message to the receiver. + + await minInstance.whatsAppDirectLine.received(req, res); + } + } catch (error) { + GBLog.error(`Error on Whatsapp callback: ${error.data ? error.data : error}`); + } + } + + /** + * Creates a listener that can be used by external monitors to check + * bot instance health. + */ + private createCheckHealthAddress(server: any, min: GBMinInstance, instance: IGBInstance) { + server.get(`/${min.instance.botId}/check`, async (req, res) => { + try { + + // Performs the checking of WhatsApp API if enabled for this instance. + + if (min.whatsAppDirectLine != undefined && instance.whatsappServiceKey !== null) { + if (!(await min.whatsAppDirectLine.check(min))) { + const error = `WhatsApp API lost connection.`; + GBLog.error(error); + res.status(500).send(error); + + return; + } + } + + // GB is OK, so 200. + + res.status(200).send(`General Bot ${min.botId} is healthly.`); + + } catch (error) { + + // GB is not OK, 500 and detail the information on response content. + + GBLog.error(error); + res.status(500).send(error.toString()); + } + }); + } + + /** + * Handle OAuth2 web service calls for token requests + * on https:////token URL. + */ + private handleOAuthTokenRequests(server: any, min: GBMinInstance, instance: IGBInstance) { + + server.get(`/${min.instance.botId}/token`, async (req, res) => { + + // Checks request state by reading AntiCSRFAttackState from GB Admin infrastructure. + + const state = await min.adminService.getValue(instance.instanceId, 'AntiCSRFAttackState'); + if (req.query.state !== state) { + const msg = 'WARNING: state field was not provided as anti-CSRF token'; + GBLog.error(msg); + throw new Error(msg); + } + const authenticationContext = new AuthenticationContext( + urlJoin(min.instance.authenticatorAuthorityHostUrl, min.instance.authenticatorTenant) + ); + const resource = 'https://graph.microsoft.com'; + + // Calls MSFT to get token. + + authenticationContext.acquireTokenWithAuthorizationCode( + req.query.code, + urlJoin(instance.botEndpoint, min.instance.botId, '/token'), + resource, + instance.marketplaceId, + instance.marketplacePassword, + async (err, token) => { + if (err) { + const msg = `handleOAuthTokenRequests: Error acquiring token: ${err}`; + GBLog.error(msg); + res.send(msg); + } else { + + // Saves token to the database. + + await this.adminService.setValue(instance.instanceId, 'accessToken', token.accessToken); + await this.adminService.setValue(instance.instanceId, 'refreshToken', token.refreshToken); + await this.adminService.setValue(instance.instanceId, 'expiresOn', token.expiresOn.toString()); + await this.adminService.setValue(instance.instanceId, 'AntiCSRFAttackState', undefined); + + // Inform the home for default .gbui after finishing token retrival. + + res.redirect(min.instance.botEndpoint); + } + } + ); + }); + } + + /** + * Handle OAuth2 web service calls for authorization requests + * on https:////auth URL. + */ + private handleOAuthRequests(server: any, min: GBMinInstance) { + server.get(`/${min.instance.botId}/auth`, (req, res) => { + let authorizationUrl = urlJoin( + min.instance.authenticatorAuthorityHostUrl, + min.instance.authenticatorTenant, + '/oauth2/authorize' + ); + authorizationUrl = `${authorizationUrl}?response_type=code&client_id=${min.instance.marketplaceId + }&redirect_uri=${urlJoin(min.instance.botEndpoint, min.instance.botId, 'token')}`; + GBLog.info(`HandleOAuthRequests: ${authorizationUrl}.`); + res.redirect(authorizationUrl); + }); + } + + /** + * Returns the instance object to clients requesting bot info. + */ + private async handleGetInstanceForClient(req: any, res: any) { + + // Translates the requested botId. + + let botId = req.params.botId; + if (botId === '[default]' || botId === undefined) { + botId = GBConfigService.get('BOT_ID'); + } + + GBLog.info(`Client requested instance for: ${botId}.`); + + // Loads by the botId itself or by the activationCode field. + + let instance = await this.core.loadInstanceByBotId(botId); + if (instance === null) { + instance = await this.core.loadInstanceByActivationCode(botId); + } + + if (instance !== null) { + + // Gets the webchat token, speech token and theme. + + const webchatTokenContainer = await this.getWebchatToken(instance); + const speechToken = instance.speechKey != undefined ? await this.getSTSToken(instance) : null; + let theme = instance.theme; + + // Sends all information to the .gbui web client. + + if (!theme) { + theme = `default.gbtheme`; + } + + + res.send( + JSON.stringify({ + instanceId: instance.instanceId, + botId: botId, + theme: theme, + webchatToken: webchatTokenContainer.token, + speechToken: speechToken, + conversationId: webchatTokenContainer.conversationId, + authenticatorTenant: instance.authenticatorTenant, + authenticatorClientId: instance.marketplaceId, + paramLogoImageUrl: this.core.getParam(instance, 'Logo Image Url', null), + paramLogoImageAlt: this.core.getParam(instance, 'Logo Image Alt', null), + paramLogoImageWidth: this.core.getParam(instance, 'Logo Image Width', null), + paramLogoImageHeight: this.core.getParam(instance, 'Logo Image Height', null), + paramLogoImageType: this.core.getParam(instance, 'Logo Image Type', null) + }) + ); + } else { + const error = `Instance not found while retrieving from .gbui web client: ${botId}.`; + res.sendStatus(error); + GBLog.error(error); + } + } + + /** + * Gets Webchat token from Bot Service. + */ + private async getWebchatToken(instance: any) { + const options = { + url: 'https://directline.botframework.com/v3/directline/tokens/generate', + method: 'POST', + headers: { + Authorization: `Bearer ${instance.webchatKey}` + } + }; + + try { + const json = await request(options); + + return JSON.parse(json); + } catch (error) { + const msg = `[botId:${instance.botId}] Error calling Direct Line to generate a token for Web control: ${error}.`; + + return Promise.reject(new Error(msg)); + } + } + + /** + * Gets a Speech to Text / Text to Speech token from the provider. + */ + private async getSTSToken(instance: any) { + const options = { + url: instance.speechEndpoint, + method: 'POST', + headers: { + 'Ocp-Apim-Subscription-Key': instance.speechKey + } + }; + + try { + return await request(options); + } catch (error) { + const msg = `Error calling Speech to Text client. Error is: ${error}.`; + + return Promise.reject(new Error(msg)); + } + } + + /** + * Builds the BOT Framework & GB infrastructures. + */ + private async buildBotAdapter(instance: any, sysPackages: IGBPackage[], appPackages: IGBPackage[]) { + + // MSFT stuff. + + const adapter = new BotFrameworkAdapter( + { appId: instance.marketplaceId, appPassword: instance.marketplacePassword }); + const storage = new MemoryStorage(); + const conversationState = new ConversationState(storage); + const userState = new UserState(storage); + adapter.use(new AutoSaveStateMiddleware(conversationState, userState)); + MicrosoftAppCredentials.trustServiceUrl('https://directline.botframework.com', + new Date(new Date().setFullYear(new Date().getFullYear() + 10)) + ); + + // The minimal bot is built here. + + const min = new GBMinInstance(); + min.botId = instance.botId; + min.bot = adapter; + min.userState = userState; + min.core = this.core; + min.conversationalService = this.conversationalService; + min.adminService = this.adminService; + min.deployService = this.deployer; + min.kbService = new KBService(this.core.sequelize); + min.instance = instance; + min.cbMap = {}; + min.scriptMap = {}; + min.sandBoxMap = {}; + min["scheduleMap"] = {}; + min["conversationWelcomed"] = {}; + min.packages = sysPackages; + min.appPackages = appPackages; + + if (GBServer.globals.minBoot === undefined) { + GBServer.globals.minBoot = min; + } + + if (min.instance.facebookWorkplaceVerifyToken) { + min['fbAdapter'] = new FacebookAdapter({ + verify_token: min.instance.facebookWorkplaceVerifyToken, + app_secret: min.instance.facebookWorkplaceAppSecret, + access_token: min.instance.facebookWorkplaceAccessToken + }); + } + // TODO: min.appPackages = core.getPackagesByInstanceId(min.instance.instanceId); + + // Creates a hub of services available in .gbapps. + + await CollectionUtil.asyncForEach(min.appPackages, async (e: IGBPackage) => { + let services: ConcatArray; + if ((services = await e.onExchangeData(min, 'getServices', null))) { + min.gbappServices = { ...min.gbappServices, ...services }; + } + }); + + if (min.instance.googlePrivateKey) { + min['googleDirectLine'] = new GoogleChatDirectLine( + min, + min.botId, + min.instance.googleBotKey, + min.instance.googleChatSubscriptionName, + min.instance.googleChatApiKey, + min.instance.googleClientEmail, + min.instance.googlePrivateKey.replace(/\\n/gm, '\n'), + min.instance.googleProjectId + ); + await min['googleDirectLine'].setup(true); + } + // If there is WhatsApp configuration specified, initialize + // infrastructure objects. + + if (min.instance.whatsappServiceUrl) { + min.whatsAppDirectLine = new WhatsappDirectLine( + min, + min.botId, + min.instance.whatsappBotKey, + min.instance.whatsappServiceKey, + min.instance.whatsappServiceNumber, + min.instance.whatsappServiceUrl + ); + await min.whatsAppDirectLine.setup(true); + } else { + const minBoot = GBServer.globals.minBoot as any; + if (minBoot.instance.whatsappServiceUrl) { + min.whatsAppDirectLine = new WhatsappDirectLine( + min, + min.botId, + min.instance.whatsappBotKey, + minBoot.instance.whatsappServiceKey, + minBoot.instance.whatsappServiceNumber, + minBoot.instance.whatsappServiceUrl + ); + await min.whatsAppDirectLine.setup(false); + } + } + + // Setups default BOT Framework dialogs. + + min.userProfile = conversationState.createProperty('userProfile'); + const dialogState = conversationState.createProperty('dialogState'); + + min.dialogs = new DialogSet(dialogState); + min.dialogs.add(new TextPrompt('textPrompt')); + min.dialogs.add(new ConfirmPrompt('confirmPrompt')); + if (process.env.ENABLE_AUTH) { + min.dialogs.add( + new OAuthPrompt('oAuthPrompt', { + connectionName: 'OAuth2', + text: 'Please sign in to General Bots.', + title: 'Sign in', + timeout: 300000 + }) + ); + } + return { min, adapter, conversationState }; + } + + /** + * Performs calling of loadBot event in all .gbapps. + */ + private async invokeLoadBot(appPackages: IGBPackage[], sysPackages: IGBPackage[], min: GBMinInstance) { + + // Calls loadBot event in all .gbapp packages. + + await CollectionUtil.asyncForEach(sysPackages, async p => { + p.sysPackages = sysPackages; + if (p.getDialogs !== undefined) { + const dialogs = await p.getDialogs(min); + if (dialogs !== undefined) { + dialogs.forEach(dialog => { + min.dialogs.add(new WaterfallDialog(dialog.id, dialog.waterfall)); + }); + } + } + + await p.loadBot(min); + }); + + // Adds all dialogs from .gbapps into global dialo list for this minimal instance. + + await CollectionUtil.asyncForEach(appPackages, async p => { + p.sysPackages = sysPackages; + await p.loadBot(min); + if (p.getDialogs !== undefined) { + const dialogs = await p.getDialogs(min); + if (dialogs !== undefined) { + dialogs.forEach(dialog => { + min.dialogs.add(new WaterfallDialog(dialog.id, dialog.waterfall)); + }); + } + } + }); + } + + // TODO: Unify in util. + public static userMobile(step) { + let mobile = WhatsappDirectLine.mobiles[step.context.activity.conversation.id] + return mobile; + + } + + + /** + * BOT Framework web service hook method. + */ + private async receiver( + req: any, + res: any, + conversationState: ConversationState, + min: GBMinInstance, + instance: any, + appPackages: any[] + ) { + + let adapter = min.bot; + + if (req.body.object) { + req['rawBody'] = JSON.stringify(req.body); + adapter = min['fbAdapter']; + } + + // Default activity processing and handler. + + await adapter['processActivity'](req, res, async context => { + + if (context.activity.text) { + context.activity.text = context.activity.text.replace(/\@General Bots Online /gi, ''); + } + + // Get loaded user state + + const step = await min.dialogs.createContext(context); + step.context.activity.locale = 'pt-BR'; + let firstTime = false; + + + try { + const user = await min.userProfile.get(context, {}); + + // First time processing. + + const sec = new SecService(); + if (!user.loaded) { + if (step.context.activity.channelId !== 'msteams') { + await min.conversationalService.sendEvent(min, step, 'loadInstance', {}); + } + + user.loaded = true; + user.subjects = []; + user.cb = undefined; + user.welcomed = false; + user.basicOptions = { maxLines: 100, translatorOn: true, wholeWord: true }; + + firstTime = true; + + // This same event is dispatched either to all participants + // including the bot, that is filtered bellow. + + if (context.activity.from.id !== min.botId) { + + // Creates a new row in user table if it does not exists. + + const member = context.activity.from; + const persistedUser = await sec.ensureUser( + instance.instanceId, + member.id, + member.name, + '', + 'web', + member.name, + null + ); + // Stores conversation associated to the user to group each message. + + const analytics = new AnalyticsService(); + user.systemUser = persistedUser; + user.conversation = await analytics.createConversation(persistedUser); + + } + + if (step.context.activity.channelId !== 'msteams') { + const service = new KBService(min.core.sequelize); + const data = await service.getFaqBySubjectArray(instance.instanceId, 'faq', undefined); + await min.conversationalService.sendEvent(min, step, 'play', { + playerType: 'bullet', + data: data.slice(0, 10) + }); } - } else { - - // User wants to switch bots. - - if (toSwitchMin !== undefined) { - - // So gets the new bot instance information and prepares to - // auto start dialog if any is specified. - - const instance = await this.core.loadInstanceByBotId(activeMin.botId); - await sec.updateUserInstance(id, instance.instanceId); - await (activeMin as any).whatsAppDirectLine.resetConversationId(id, ''); - const startDialog = activeMin.core.getParam(activeMin.instance, 'Start Dialog', null); - - - if (startDialog) { - GBLog.info(`Calling /start for Auto start : ${startDialog} for ${activeMin.instance.botId}...`); - req.body.messages[0].body = `/start`; - await (activeMin as any).whatsAppDirectLine.received(req, res); - } else { - await (activeMin as any).whatsAppDirectLine.sendToDevice( - id, - `Agora falando com ${activeMin.instance.title}...`, - null - ); - - - } - res.end(); - } else { - activeMin = GBServer.globals.minInstances.filter(p => p.instance.instanceId === user.instanceId)[0]; - if (activeMin === undefined) { - activeMin = GBServer.globals.minBoot; - await (activeMin as any).whatsAppDirectLine.sendToDevice( - id, - `O outro Bot que você estava falando(${user.instanceId}), não está mais disponível. Agora você está falando comigo, ${activeMin.instance.title}...` - ); - } - await (activeMin as any).whatsAppDirectLine.received(req, res); - } - } - } else { - let minInstance = GBServer.globals.minInstances.filter( - p => p.instance.botId.toLowerCase() === botId - )[0]; - - - // Just pass the message to the receiver. - - await minInstance.whatsAppDirectLine.received(req, res); - } - } catch (error) { - GBLog.error(`Error on Whatsapp callback: ${error.data ? error.data : error}`); - } - } - - /** - * Creates a listener that can be used by external monitors to check - * bot instance health. - */ - private createCheckHealthAddress(server: any, min: GBMinInstance, instance: IGBInstance) { - server.get(`/${min.instance.botId}/check`, async (req, res) => { - try { - - // Performs the checking of WhatsApp API if enabled for this instance. - - if (min.whatsAppDirectLine != undefined && instance.whatsappServiceKey !== null) { - if (!(await min.whatsAppDirectLine.check(min))) { - const error = `WhatsApp API lost connection.`; - GBLog.error(error); - res.status(500).send(error); - - return; - } - } - - // GB is OK, so 200. - - res.status(200).send(`General Bot ${min.botId} is healthly.`); - - } catch (error) { - - // GB is not OK, 500 and detail the information on response content. - - GBLog.error(error); - res.status(500).send(error.toString()); - } - }); - } - - /** - * Handle OAuth2 web service calls for token requests - * on https:////token URL. - */ - private handleOAuthTokenRequests(server: any, min: GBMinInstance, instance: IGBInstance) { - - server.get(`/${min.instance.botId}/token`, async (req, res) => { - - // Checks request state by reading AntiCSRFAttackState from GB Admin infrastructure. - - const state = await min.adminService.getValue(instance.instanceId, 'AntiCSRFAttackState'); - if (req.query.state !== state) { - const msg = 'WARNING: state field was not provided as anti-CSRF token'; - GBLog.error(msg); - throw new Error(msg); - } - const authenticationContext = new AuthenticationContext( - urlJoin(min.instance.authenticatorAuthorityHostUrl, min.instance.authenticatorTenant) - ); - const resource = 'https://graph.microsoft.com'; - - // Calls MSFT to get token. - - authenticationContext.acquireTokenWithAuthorizationCode( - req.query.code, - urlJoin(instance.botEndpoint, min.instance.botId, '/token'), - resource, - instance.marketplaceId, - instance.marketplacePassword, - async (err, token) => { - if (err) { - const msg = `handleOAuthTokenRequests: Error acquiring token: ${err}`; - GBLog.error(msg); - res.send(msg); - } else { - - // Saves token to the database. - - await this.adminService.setValue(instance.instanceId, 'accessToken', token.accessToken); - await this.adminService.setValue(instance.instanceId, 'refreshToken', token.refreshToken); - await this.adminService.setValue(instance.instanceId, 'expiresOn', token.expiresOn.toString()); - await this.adminService.setValue(instance.instanceId, 'AntiCSRFAttackState', undefined); - - // Inform the home for default .gbui after finishing token retrival. - - res.redirect(min.instance.botEndpoint); - } - } - ); - }); - } - - /** - * Handle OAuth2 web service calls for authorization requests - * on https:////auth URL. - */ - private handleOAuthRequests(server: any, min: GBMinInstance) { - server.get(`/${min.instance.botId}/auth`, (req, res) => { - let authorizationUrl = urlJoin( - min.instance.authenticatorAuthorityHostUrl, - min.instance.authenticatorTenant, - '/oauth2/authorize' - ); - authorizationUrl = `${authorizationUrl}?response_type=code&client_id=${min.instance.marketplaceId - }&redirect_uri=${urlJoin(min.instance.botEndpoint, min.instance.botId, 'token')}`; - GBLog.info(`HandleOAuthRequests: ${authorizationUrl}.`); - res.redirect(authorizationUrl); - }); - } - - /** - * Returns the instance object to clients requesting bot info. - */ - private async handleGetInstanceForClient(req: any, res: any) { - - // Translates the requested botId. - - let botId = req.params.botId; - if (botId === '[default]' || botId === undefined) { - botId = GBConfigService.get('BOT_ID'); - } - - GBLog.info(`Client requested instance for: ${botId}.`); - - // Loads by the botId itself or by the activationCode field. - - let instance = await this.core.loadInstanceByBotId(botId); - if (instance === null) { - instance = await this.core.loadInstanceByActivationCode(botId); - } - - if (instance !== null) { - - // Gets the webchat token, speech token and theme. - - const webchatTokenContainer = await this.getWebchatToken(instance); - const speechToken = instance.speechKey != undefined ? await this.getSTSToken(instance) : null; - let theme = instance.theme; - - // Sends all information to the .gbui web client. - - if (!theme) { - theme = `default.gbtheme`; - } - - - res.send( - JSON.stringify({ - instanceId: instance.instanceId, - botId: botId, - theme: theme, - webchatToken: webchatTokenContainer.token, - speechToken: speechToken, - conversationId: webchatTokenContainer.conversationId, - authenticatorTenant: instance.authenticatorTenant, - authenticatorClientId: instance.marketplaceId, - paramLogoImageUrl: this.core.getParam(instance, 'Logo Image Url', null), - paramLogoImageAlt: this.core.getParam(instance, 'Logo Image Alt', null), - paramLogoImageWidth: this.core.getParam(instance, 'Logo Image Width', null), - paramLogoImageHeight: this.core.getParam(instance, 'Logo Image Height', null), - paramLogoImageType: this.core.getParam(instance, 'Logo Image Type', null) - }) - ); - } else { - const error = `Instance not found while retrieving from .gbui web client: ${botId}.`; - res.sendStatus(error); - GBLog.error(error); - } - } - - /** - * Gets Webchat token from Bot Service. - */ - private async getWebchatToken(instance: any) { - const options = { - url: 'https://directline.botframework.com/v3/directline/tokens/generate', - method: 'POST', - headers: { - Authorization: `Bearer ${instance.webchatKey}` - } - }; - - try { - const json = await request(options); - - return JSON.parse(json); - } catch (error) { - const msg = `[botId:${instance.botId}] Error calling Direct Line to generate a token for Web control: ${error}.`; - - return Promise.reject(new Error(msg)); - } - } - - /** - * Gets a Speech to Text / Text to Speech token from the provider. - */ - private async getSTSToken(instance: any) { - const options = { - url: instance.speechEndpoint, - method: 'POST', - headers: { - 'Ocp-Apim-Subscription-Key': instance.speechKey - } - }; - - try { - return await request(options); - } catch (error) { - const msg = `Error calling Speech to Text client. Error is: ${error}.`; - - return Promise.reject(new Error(msg)); - } - } - - /** - * Builds the BOT Framework & GB infrastructures. - */ - private async buildBotAdapter(instance: any, sysPackages: IGBPackage[], appPackages: IGBPackage[]) { - - // MSFT stuff. - - const adapter = new BotFrameworkAdapter( - { appId: instance.marketplaceId, appPassword: instance.marketplacePassword }); - const storage = new MemoryStorage(); - const conversationState = new ConversationState(storage); - const userState = new UserState(storage); - adapter.use(new AutoSaveStateMiddleware(conversationState, userState)); - MicrosoftAppCredentials.trustServiceUrl('https://directline.botframework.com', - new Date(new Date().setFullYear(new Date().getFullYear() + 10)) - ); - - // The minimal bot is built here. - - const min = new GBMinInstance(); - min.botId = instance.botId; - min.bot = adapter; - min.userState = userState; - min.core = this.core; - min.conversationalService = this.conversationalService; - min.adminService = this.adminService; - min.deployService = this.deployer; - min.kbService = new KBService(this.core.sequelize); - min.instance = instance; - min.cbMap = {}; - min.scriptMap = {}; - min.sandBoxMap = {}; - min["scheduleMap"] = {}; - min["conversationWelcomed"] = {}; - min.packages = sysPackages; - min.appPackages = appPackages; - - if (GBServer.globals.minBoot === undefined) { - GBServer.globals.minBoot = min; - } - - if (min.instance.facebookWorkplaceVerifyToken) { - min['fbAdapter'] = new FacebookAdapter({ - verify_token: min.instance.facebookWorkplaceVerifyToken, - app_secret: min.instance.facebookWorkplaceAppSecret, - access_token: min.instance.facebookWorkplaceAccessToken - }); - } - // TODO: min.appPackages = core.getPackagesByInstanceId(min.instance.instanceId); - - // Creates a hub of services available in .gbapps. - - await CollectionUtil.asyncForEach(min.appPackages, async (e: IGBPackage) => { - let services: ConcatArray; - if ((services = await e.onExchangeData(min, 'getServices', null))) { - min.gbappServices = { ...min.gbappServices, ...services }; - } - }); - - if (min.instance.googlePrivateKey) { - min['googleDirectLine'] = new GoogleChatDirectLine( - min, - min.botId, - min.instance.googleBotKey, - min.instance.googleChatSubscriptionName, - min.instance.googleChatApiKey, - min.instance.googleClientEmail, - min.instance.googlePrivateKey.replace(/\\n/gm, '\n'), - min.instance.googleProjectId - ); - await min['googleDirectLine'].setup(true); - } - // If there is WhatsApp configuration specified, initialize - // infrastructure objects. - - if (min.instance.whatsappServiceUrl) { - min.whatsAppDirectLine = new WhatsappDirectLine( - min, - min.botId, - min.instance.whatsappBotKey, - min.instance.whatsappServiceKey, - min.instance.whatsappServiceNumber, - min.instance.whatsappServiceUrl - ); - await min.whatsAppDirectLine.setup(true); - } else { - const minBoot = GBServer.globals.minBoot as any; - if (minBoot.instance.whatsappServiceUrl) { - min.whatsAppDirectLine = new WhatsappDirectLine( - min, - min.botId, - min.instance.whatsappBotKey, - minBoot.instance.whatsappServiceKey, - minBoot.instance.whatsappServiceNumber, - minBoot.instance.whatsappServiceUrl - ); - await min.whatsAppDirectLine.setup(false); - } - } - - // Setups default BOT Framework dialogs. - - min.userProfile = conversationState.createProperty('userProfile'); - const dialogState = conversationState.createProperty('dialogState'); - - min.dialogs = new DialogSet(dialogState); - min.dialogs.add(new TextPrompt('textPrompt')); - min.dialogs.add(new ConfirmPrompt('confirmPrompt')); - if (process.env.ENABLE_AUTH) { - min.dialogs.add( - new OAuthPrompt('oAuthPrompt', { - connectionName: 'OAuth2', - text: 'Please sign in to General Bots.', - title: 'Sign in', - timeout: 300000 - }) - ); - } - return { min, adapter, conversationState }; - } - - /** - * Performs calling of loadBot event in all .gbapps. - */ - private async invokeLoadBot(appPackages: IGBPackage[], sysPackages: IGBPackage[], min: GBMinInstance) { - - // Calls loadBot event in all .gbapp packages. - - await CollectionUtil.asyncForEach(sysPackages, async p => { - p.sysPackages = sysPackages; - if (p.getDialogs !== undefined) { - const dialogs = await p.getDialogs(min); - if (dialogs !== undefined) { - dialogs.forEach(dialog => { - min.dialogs.add(new WaterfallDialog(dialog.id, dialog.waterfall)); - }); - } - } - - await p.loadBot(min); - }); - - // Adds all dialogs from .gbapps into global dialo list for this minimal instance. - - await CollectionUtil.asyncForEach(appPackages, async p => { - p.sysPackages = sysPackages; - await p.loadBot(min); - if (p.getDialogs !== undefined) { - const dialogs = await p.getDialogs(min); - if (dialogs !== undefined) { - dialogs.forEach(dialog => { - min.dialogs.add(new WaterfallDialog(dialog.id, dialog.waterfall)); - }); - } - } - }); - } - - // TODO: Unify in util. - public static userMobile(step) { - let mobile = WhatsappDirectLine.mobiles[step.context.activity.conversation.id] - return mobile; - - } - - - /** - * BOT Framework web service hook method. - */ - private async receiver( - req: any, - res: any, - conversationState: ConversationState, - min: GBMinInstance, - instance: any, - appPackages: any[] - ) { - - let adapter = min.bot; - - if (req.body.object) { - req['rawBody'] = JSON.stringify(req.body); - adapter = min['fbAdapter']; - } - - // Default activity processing and handler. - - await adapter['processActivity'](req, res, async context => { - - if (context.activity.text) { - context.activity.text = context.activity.text.replace(/\@General Bots Online /gi, ''); - } - - // Get loaded user state - - const step = await min.dialogs.createContext(context); - step.context.activity.locale = 'pt-BR'; - let firstTime = false; - - - try { - const user = await min.userProfile.get(context, {}); - - // First time processing. - - const sec = new SecService(); - if (!user.loaded) { - - await min.conversationalService.sendEvent(min, step, 'loadInstance', {}); - - user.loaded = true; - user.subjects = []; - user.cb = undefined; - user.welcomed = false; - user.basicOptions = { maxLines: 100, translatorOn: true, wholeWord: true }; - - firstTime = true; - - // This same event is dispatched either to all participants - // including the bot, that is filtered bellow. - - if (context.activity.from.id !== min.botId) { - - // Creates a new row in user table if it does not exists. - - const member = context.activity.from; - const persistedUser = await sec.ensureUser( - instance.instanceId, - member.id, - member.name, - '', - 'web', - member.name, - null - ); - // Stores conversation associated to the user to group each message. - - const analytics = new AnalyticsService(); - user.systemUser = persistedUser; - user.conversation = await analytics.createConversation(persistedUser); - - } - - const service = new KBService(min.core.sequelize); - const data = await service.getFaqBySubjectArray(instance.instanceId, 'faq', undefined); - await min.conversationalService.sendEvent(min, step, 'play', { - playerType: 'bullet', - data: data.slice(0, 10) - }); - - // Saves session user (persisted GuaribasUser is inside). - - await min.userProfile.set(step.context, user); - } - - user.systemUser = await sec.getUserFromSystemId(user.systemUser.userSystemId); - await min.userProfile.set(step.context, user); - - // Required for MSTEAMS handling of persisted conversations. - - if (step.context.activity.channelId === 'msteams') { - - if (step.context.activity.attachments && step.context.activity.attachments.length > 1) { - - const file = context.activity.attachments[0]; - const credentials = new MicrosoftAppCredentials(min.instance.marketplaceId, min.instance.marketplacePassword); - const botToken = await credentials.getToken(); - const headers = { Authorization: `Bearer ${botToken}` }; - const t = new SystemKeywords(null, null, null); - const data = await t.getByHttp(file.contentUrl, headers, null, null, null, true); - const folder = `work/${min.instance.botId}.gbai/cache`; - const filename = `${GBAdminService.generateUuid()}.png`; - - if (!Fs.existsSync(folder)) { - Fs.mkdirSync(folder); - } - - Fs.writeFileSync(path.join(folder, filename), data); - step.context.activity.text = urlJoin(GBServer.globals.publicAddress, `${min.instance.botId}`, 'cache', filename); - } - - const conversationReference = JSON.stringify( - TurnContext.getConversationReference(context.activity) - ); - await sec.updateConversationReferenceById(user.systemUser.userId, conversationReference); - - if (!user.welcomed) { - const startDialog = min.core.getParam(min.instance, 'Start Dialog', null); - if (startDialog && !user.welcomed) { - user.welcomed = true; - GBLog.info(`Auto start (teams) dialog is now being called: ${startDialog} for ${min.instance.botId}...`); - await GBVMService.callVM(startDialog.toLowerCase(), min, step, this.deployer); - } - } - } - - // Required for F0 handling of persisted conversations. - - GBLog.info(`User>: text:${context.activity.text} (type: ${context.activity.type}, name: ${context.activity.name}, channelId: ${context.activity.channelId}, value: ${context.activity.value})`); - - // Answer to specific BOT Framework event conversationUpdate to auto start dialogs. - // Skips if the bot is talking. - const startDialog = min.core.getParam(min.instance, 'Start Dialog', null); - - if (context.activity.type === 'installationUpdate') { - GBLog.info(`Bot installed on Teams.`); - } else if (context.activity.type === 'conversationUpdate' && - context.activity.membersAdded.length > 0) { - - // Check if a bot or a human participant is being added to the conversation. - - const member = context.activity.membersAdded[0]; - if (context.activity.membersAdded[0].id === context.activity.recipient.id) { - GBLog.info(`Bot added to conversation, starting chat...`); - - // Calls onNewSession event on each .gbapp package. - - await CollectionUtil.asyncForEach(appPackages, async e => { - await e.onNewSession(min, step); - }); - - // Auto starts dialogs if any is specified. - - if (!startDialog && !user.welcomed) { - - // Otherwise, calls / (root) to default welcome users. - - await step.beginDialog('/'); - } - else { - if (!GBMinService.userMobile(step) && - !min["conversationWelcomed"][step.context.activity.conversation.id]) { - - min["conversationWelcomed"][step.context.activity.conversation.id] = true; - - GBLog.info(`Auto start (web 1) dialog is now being called: ${startDialog} for ${min.instance.instanceId}...`); - await GBVMService.callVM(startDialog.toLowerCase(), min, step, this.deployer); - } - } - - } else { - GBLog.info(`Person added to conversation: ${member.name}`); - - if (GBMinService.userMobile(step)) { - if (startDialog && !min["conversationWelcomed"][step.context.activity.conversation.id] && - !step.context.activity['group']) { - user.welcomed = true; - min["conversationWelcomed"][step.context.activity.conversation.id] = true; - await min.userProfile.set(step.context, user); - GBLog.info(`Auto start (whatsapp) dialog is now being called: ${startDialog} for ${min.instance.instanceId}...`); - await GBVMService.callVM(startDialog.toLowerCase(), min, step, this.deployer); - } - } - } - - } else if (context.activity.type === 'message') { - - // Processes messages activities. - - await this.processMessageActivity(context, min, step); - - } else if (context.activity.type === 'event') { - - // Processes events activities. - - await this.processEventActivity(min, user, context, step); - } - - // Saves conversation state for later use. - - await conversationState.saveChanges(context, true); - - } catch (error) { - - const msg = `ERROR: ${error.message} ${error.stack ? error.stack : ''}`; - GBLog.error(msg); - - await min.conversationalService.sendText( - min, - step, - Messages[step.context.activity.locale].very_sorry_about_error - ); - - await step.beginDialog('/ask', { isReturning: true }); - } - }); - } - - /** - * Called to handle all event sent by .gbui clients. - */ - private async processEventActivity(min, user, context, step: GBDialogStep) { - - if (context.activity.name === 'whoAmI') { - await step.beginDialog('/whoAmI'); - } else if (context.activity.name === 'showSubjects') { - await step.beginDialog('/menu', undefined); - } else if (context.activity.name === 'giveFeedback') { - await step.beginDialog('/feedback', { - fromMenu: true - }); - } else if (context.activity.name === 'showFAQ') { - await step.beginDialog('/faq'); - } else if (context.activity.name === 'answerEvent') { - await step.beginDialog('/answerEvent', { - questionId: context.activity.data, - fromFaq: true - }); - } else if (context.activity.name === 'quality') { - await step.beginDialog('/quality', { - score: context.activity.data - }); - } else if (context.activity.name === 'startGB') { - const startDialog = min.core.getParam(min.instance, 'Start Dialog', null); - if (startDialog && !min["conversationWelcomed"][step.context.activity.conversation.id]) { - user.welcomed = true; - GBLog.info(`Auto start (web 2) dialog is now being called: ${startDialog} for ${min.instance.instanceId}...`); - await GBVMService.callVM(startDialog.toLowerCase(), min, step, this.deployer); - } - } else if (context.activity.name === 'updateToken') { - const token = context.activity.data; - await step.beginDialog('/adminUpdateToken', { token: token }); - } else { - await step.continueDialog(); - } - } - - /** - * Called to handle all text messages sent and received by the bot. - */ - private async processMessageActivity(context, min: GBMinInstance, step: GBDialogStep) { - - const sec = new SecService(); - - // Removes Bot Id from MS Teams. - - context.activity.text = context.activity.text.replace(/\.*\<\/at\>\s/gi, ''); - - let data = { query: context.activity.text }; - await CollectionUtil.asyncForEach(min.appPackages, async (e: IGBPackage) => { - await e.onExchangeData(min, 'handleRawInput', data); - // TODO: Handle priority over .gbapp, today most common case is just one item per server. - }); - context.activity.text = data.query; - - // Additional clean up. - - context.activity.text = context.activity.text.trim(); - - const user = await min.userProfile.get(context, {}); - let message: GuaribasConversationMessage; - if (process.env.PRIVACY_STORE_MESSAGES === 'true') { - - // Adds message to the analytics layer. - - const analytics = new AnalyticsService(); - if (user) { - - if (!user.conversation) { - user.conversation = await analytics.createConversation(user.systemUser); - } - - message = await analytics.createMessage( - min.instance.instanceId, - user.conversation, - user.systemUser.userId, - context.activity.text - ); - } - } - - // Checks for global exit kewywords cancelling any active dialogs. - - const globalQuit = (locale, utterance) => { - return utterance.match(Messages.global_quit); - }; - - // Files in .gbdialog can be called directly by typing its name normalized into JS . - - const isVMCall = Object.keys(min.scriptMap).find(key => min.scriptMap[key] === context.activity.text) !== undefined; - if (isVMCall) { - await GBVMService.callVM(context.activity.text, min, step, this.deployer); - } else if (context.activity.text.charAt(0) === '/') { - - const text = context.activity.text; - const parts = text.split(' '); - const cmdOrDialogName = parts[0]; - parts.splice(0, 1); - const args = parts.join(' '); - if (cmdOrDialogName === '/start') { - - - // Reset user. - - const user = await min.userProfile.get(context, {}); - await min.conversationalService.sendEvent(min, step, 'loadInstance', {}); - user.loaded = false; - await min.userProfile.set(step.context, user); - - } else if (cmdOrDialogName === '/call') { - await GBVMService.callVM(args, min, step, this.deployer); - } else { - await step.beginDialog(cmdOrDialogName, { args: args }); - } - } else if (globalQuit(step.context.activity.locale, context.activity.text)) { - await step.cancelAllDialogs(); - await min.conversationalService.sendText(min, step, Messages[step.context.activity.locale].canceled); - - } else if (context.activity.text === 'admin') { - await step.beginDialog('/admin'); - - } else if (context.activity.text.startsWith('{"title"')) { - await step.beginDialog('/menu', JSON.parse(context.activity.text)); - - } else if ( - !(await this.deployer.getStoragePackageByName(min.instance.instanceId, `${min.instance.botId}.gbkb`)) && - process.env.GBKB_ENABLE_AUTO_PUBLISH === 'true' - ) { - await min.conversationalService.sendText(min, step, - `Oi, ainda não possuo pacotes de conhecimento publicados. Por favor, aguarde alguns segundos enquanto eu auto-publico alguns pacotes.` - ); - await step.beginDialog('/publish', { confirm: true, firstTime: true }); - } else { - - // Removes unwanted chars in input text. - - let text = context.activity.text; - const originalText = text; - text = text.replace(/<([^>]+?)([^>]*?)>(.*?)<\/\1>/gi, ''); - - // Saves special words (keep text) in tokens to prevent it from - // spell checking and translation. - - const keepText: string = min.core.getParam(min.instance, 'Keep Text', ''); - let keepTextList = []; - if (keepTextList) { - keepTextList = keepTextList.concat(keepText.split(';')); - } - const replacements = []; - await CollectionUtil.asyncForEach(min.appPackages, async (e: IGBPackage) => { - const result = await e.onExchangeData(min, 'getKeepText', {}); - if (result) { - keepTextList = keepTextList.concat(result); - } - }); - - const getNormalizedRegExp = (value) => { - var chars = [ - { letter: 'a', reg: '[aáàãäâ]' }, - { letter: 'e', reg: '[eéèëê]' }, - { letter: 'i', reg: '[iíìïî]' }, - { letter: 'o', reg: '[oóòõöô]' }, - { letter: 'u', reg: '[uúùüû]' }, - { letter: 'c', reg: '[cç]' } - ]; - - for (var i in chars) { - value = value.replace(new RegExp(chars[i].letter, 'gi'), chars[i].reg); - }; - return value; - }; - - let textProcessed = text; - if (keepTextList) { - keepTextList = keepTextList.filter(p => p.trim() !== ''); - let i = 0; - await CollectionUtil.asyncForEach(keepTextList, item => { - const it = GBConversationalService.removeDiacritics(item); - const noAccentText = GBConversationalService.removeDiacritics(textProcessed); - - if (noAccentText.toLowerCase().indexOf(it.toLowerCase()) != -1) { - const replacementToken = 'X' + GBAdminService.getNumberIdentifier().substr(0, 4); - replacements[i] = { text: item, replacementToken: replacementToken }; - i++; - textProcessed = textProcessed.replace(new RegExp(`\\b${getNormalizedRegExp(it.trim())}\\b`, 'gi'), `${replacementToken}`); - } - }); - } - - // Spells check the input text before translating, - // keeping fixed tokens as specified in Config. - - text = await min.conversationalService.spellCheck(min, textProcessed); - - // Detects user typed language and updates their locale profile if applies. - - let locale = min.core.getParam(min.instance, 'Default User Language', - GBConfigService.get('DEFAULT_USER_LANGUAGE') - ); - const detectLanguage = min.core.getParam(min.instance, 'Language Detector', - GBConfigService.getBoolean('LANGUAGE_DETECTOR') - ) === 'true'; - const systemUser = user.systemUser; - locale = systemUser.locale; - if (detectLanguage || !locale) { - locale = await min.conversationalService.getLanguage(min, text); - if (systemUser.locale != locale) { - - user.systemUser = await sec.updateUserLocale(systemUser.userId, locale); - await min.userProfile.set(step.context, user); - } - } - - // Checks for bad words on input text. - - const hasBadWord = wash.check(locale, context.activity.text); - if (hasBadWord) { - return await step.beginDialog('/pleaseNoBadWords'); - } - - // Translates text into content language, keeping - // reserved tokens specified in Config. - - const contentLocale = min.core.getParam( - min.instance, - 'Default Content Language', - GBConfigService.get('DEFAULT_CONTENT_LANGUAGE') - ); - text = await min.conversationalService.translate(min, text, contentLocale); - GBLog.info(`Translated text (processMessageActivity): ${text}.`); - - // Restores all token text back after spell checking and translation. - - if (keepTextList) { - let i = 0; - await CollectionUtil.asyncForEach(replacements, item => { - i++; - text = text.replace(new RegExp(`${item.replacementToken}`, 'gi'), item.text); - }); - } - step.context.activity['text'] = text; - step.context.activity['originalText'] = originalText; - - GBLog.info(`Final text ready for NLP/Search/.gbapp: ${text}.`); - - if (user.systemUser.agentMode === 'self') { - const manualUser = await sec.getUserFromAgentSystemId(user.systemUser.userSystemId); - - GBLog.info(`HUMAN AGENT (${user.systemUser.userSystemId}) TO USER ${manualUser.userSystemId}: ${text}`); - - const cmd = 'SEND FILE '; - if (text.startsWith(cmd)) { - const filename = text.substr(cmd.length); - const message = await min.kbService.getAnswerTextByMediaName(min.instance.instanceId, filename); - - if (message === null) { - GBLog.error(`File ${filename} not found in any .gbkb published. Check the name or publish again the associated .gbkb.`); - } else { - await min.conversationalService.sendMarkdownToMobile(min, null, manualUser.userSystemId, message); - } - } - else { - await min.whatsAppDirectLine.sendToDeviceEx(manualUser.userSystemId, `${manualUser.agentSystemId}: ${text}`, locale, - step.context.activity.conversation.id); - } - } - else { - - // If there is a dialog in course, continue to the next step. - - if (step.activeDialog !== undefined) { - await step.continueDialog(); - } else { - - const startDialog = user.hearOnDialog ? - user.hearOnDialog : - min.core.getParam(min.instance, 'Start Dialog', null); - - if (text !== startDialog) { - let nextDialog = null; - let data = { - query: text, - step: step, - notTranslatedQuery: originalText, - message: message ? message['dataValues'] : null, - user: user ? user.dataValues : null - }; - await CollectionUtil.asyncForEach(min.appPackages, async (e: IGBPackage) => { - if (!nextDialog) { - nextDialog = await e.onExchangeData(min, 'handleAnswer', data); - } - }); - data.step = null; - GBLog.info(`/answer being called from processMessageActivity (nextDialog=${nextDialog}).`); - await step.beginDialog(nextDialog ? nextDialog : '/answer', { - data: data, - query: text, - user: user ? user.dataValues : null, - message: message - }); - - } - } - } - } - } -} + + // Saves session user (persisted GuaribasUser is inside). + + await min.userProfile.set(step.context, user); + } + + user.systemUser = await sec.getUserFromSystemId(user.systemUser.userSystemId); + await min.userProfile.set(step.context, user); + + // Required for MSTEAMS handling of persisted conversations. + + if (step.context.activity.channelId === 'msteams') { + + if (step.context.activity.attachments && step.context.activity.attachments.length > 1) { + + const file = context.activity.attachments[0]; + const credentials = new MicrosoftAppCredentials(min.instance.marketplaceId, min.instance.marketplacePassword); + const botToken = await credentials.getToken(); + const headers = { Authorization: `Bearer ${botToken}` }; + const t = new SystemKeywords(null, null, null); + const data = await t.getByHttp(file.contentUrl, headers, null, null, null, true); + const folder = `work/${min.instance.botId}.gbai/cache`; + const filename = `${GBAdminService.generateUuid()}.png`; + + if (!Fs.existsSync(folder)) { + Fs.mkdirSync(folder); + } + + Fs.writeFileSync(path.join(folder, filename), data); + step.context.activity.text = urlJoin(GBServer.globals.publicAddress, `${min.instance.botId}`, 'cache', filename); + } + + const conversationReference = JSON.stringify( + TurnContext.getConversationReference(context.activity) + ); + await sec.updateConversationReferenceById(user.systemUser.userId, conversationReference); + + if (!user.welcomed) { + const startDialog = min.core.getParam(min.instance, 'Start Dialog', null); + if (startDialog && !user.welcomed) { + user.welcomed = true; + GBLog.info(`Auto start (teams) dialog is now being called: ${startDialog} for ${min.instance.botId}...`); + await GBVMService.callVM(startDialog.toLowerCase(), min, step, this.deployer); + } + } + } + + // Required for F0 handling of persisted conversations. + + GBLog.info(`User>: text:${context.activity.text} (type: ${context.activity.type}, name: ${context.activity.name}, channelId: ${context.activity.channelId}, value: ${context.activity.value})`); + + // Answer to specific BOT Framework event conversationUpdate to auto start dialogs. + // Skips if the bot is talking. + const startDialog = min.core.getParam(min.instance, 'Start Dialog', null); + + if (context.activity.type === 'installationUpdate') { + GBLog.info(`Bot installed on Teams.`); + } else if (context.activity.type === 'conversationUpdate' && + context.activity.membersAdded.length > 0) { + + // Check if a bot or a human participant is being added to the conversation. + + const member = context.activity.membersAdded[0]; + if (context.activity.membersAdded[0].id === context.activity.recipient.id) { + GBLog.info(`Bot added to conversation, starting chat...`); + + // Calls onNewSession event on each .gbapp package. + + await CollectionUtil.asyncForEach(appPackages, async e => { + await e.onNewSession(min, step); + }); + + // Auto starts dialogs if any is specified. + + if (!startDialog && !user.welcomed) { + + // Otherwise, calls / (root) to default welcome users. + + await step.beginDialog('/'); + } + else { + if (!GBMinService.userMobile(step) && + !min["conversationWelcomed"][step.context.activity.conversation.id]) { + + min["conversationWelcomed"][step.context.activity.conversation.id] = true; + + GBLog.info(`Auto start (web 1) dialog is now being called: ${startDialog} for ${min.instance.instanceId}...`); + await GBVMService.callVM(startDialog.toLowerCase(), min, step, this.deployer); + } + } + + } else { + GBLog.info(`Person added to conversation: ${member.name}`); + + if (GBMinService.userMobile(step)) { + if (startDialog && !min["conversationWelcomed"][step.context.activity.conversation.id] && + !step.context.activity['group']) { + user.welcomed = true; + min["conversationWelcomed"][step.context.activity.conversation.id] = true; + await min.userProfile.set(step.context, user); + GBLog.info(`Auto start (whatsapp) dialog is now being called: ${startDialog} for ${min.instance.instanceId}...`); + await GBVMService.callVM(startDialog.toLowerCase(), min, step, this.deployer); + } + } + } + + } else if (context.activity.type === 'message') { + + // Processes messages activities. + + await this.processMessageActivity(context, min, step); + + } else if (context.activity.type === 'event') { + + // Processes events activities. + + await this.processEventActivity(min, user, context, step); + } + + // Saves conversation state for later use. + + await conversationState.saveChanges(context, true); + + } catch (error) { + + const msg = `ERROR: ${error.message} ${error.stack ? error.stack : ''}`; + GBLog.error(msg); + + await min.conversationalService.sendText( + min, + step, + Messages[step.context.activity.locale].very_sorry_about_error + ); + + await step.beginDialog('/ask', { isReturning: true }); + } + }); + } + + /** + * Called to handle all event sent by .gbui clients. + */ + private async processEventActivity(min, user, context, step: GBDialogStep) { + + if (context.activity.name === 'whoAmI') { + await step.beginDialog('/whoAmI'); + } else if (context.activity.name === 'showSubjects') { + await step.beginDialog('/menu', undefined); + } else if (context.activity.name === 'giveFeedback') { + await step.beginDialog('/feedback', { + fromMenu: true + }); + } else if (context.activity.name === 'showFAQ') { + await step.beginDialog('/faq'); + } else if (context.activity.name === 'answerEvent') { + await step.beginDialog('/answerEvent', { + questionId: context.activity.data, + fromFaq: true + }); + } else if (context.activity.name === 'quality') { + await step.beginDialog('/quality', { + score: context.activity.data + }); + } else if (context.activity.name === 'startGB') { + const startDialog = min.core.getParam(min.instance, 'Start Dialog', null); + if (startDialog && !min["conversationWelcomed"][step.context.activity.conversation.id]) { + user.welcomed = true; + GBLog.info(`Auto start (web 2) dialog is now being called: ${startDialog} for ${min.instance.instanceId}...`); + await GBVMService.callVM(startDialog.toLowerCase(), min, step, this.deployer); + } + } else if (context.activity.name === 'updateToken') { + const token = context.activity.data; + await step.beginDialog('/adminUpdateToken', { token: token }); + } else { + await step.continueDialog(); + } + } + + /** + * Called to handle all text messages sent and received by the bot. + */ + private async processMessageActivity(context, min: GBMinInstance, step: GBDialogStep) { + + const sec = new SecService(); + + // Removes Bot Id from MS Teams. + + context.activity.text = context.activity.text.replace(/\.*\<\/at\>\s/gi, ''); + + let data = { query: context.activity.text }; + await CollectionUtil.asyncForEach(min.appPackages, async (e: IGBPackage) => { + await e.onExchangeData(min, 'handleRawInput', data); + // TODO: Handle priority over .gbapp, today most common case is just one item per server. + }); + context.activity.text = data.query; + + // Additional clean up. + + context.activity.text = context.activity.text.trim(); + + const user = await min.userProfile.get(context, {}); + let message: GuaribasConversationMessage; + if (process.env.PRIVACY_STORE_MESSAGES === 'true') { + + // Adds message to the analytics layer. + + const analytics = new AnalyticsService(); + if (user) { + + if (!user.conversation) { + user.conversation = await analytics.createConversation(user.systemUser); + } + + message = await analytics.createMessage( + min.instance.instanceId, + user.conversation, + user.systemUser.userId, + context.activity.text + ); + } + } + + // Checks for global exit kewywords cancelling any active dialogs. + + const globalQuit = (locale, utterance) => { + return utterance.match(Messages.global_quit); + }; + + // Files in .gbdialog can be called directly by typing its name normalized into JS . + + const isVMCall = Object.keys(min.scriptMap).find(key => min.scriptMap[key] === context.activity.text) !== undefined; + if (isVMCall) { + await GBVMService.callVM(context.activity.text, min, step, this.deployer); + } else if (context.activity.text.charAt(0) === '/') { + + const text = context.activity.text; + const parts = text.split(' '); + const cmdOrDialogName = parts[0]; + parts.splice(0, 1); + const args = parts.join(' '); + if (cmdOrDialogName === '/start') { + + + // Reset user. + + const user = await min.userProfile.get(context, {}); + await min.conversationalService.sendEvent(min, step, 'loadInstance', {}); + user.loaded = false; + await min.userProfile.set(step.context, user); + + } else if (cmdOrDialogName === '/call') { + await GBVMService.callVM(args, min, step, this.deployer); + } else { + await step.beginDialog(cmdOrDialogName, { args: args }); + } + } else if (globalQuit(step.context.activity.locale, context.activity.text)) { + await step.cancelAllDialogs(); + await min.conversationalService.sendText(min, step, Messages[step.context.activity.locale].canceled); + + } else if (context.activity.text === 'admin') { + await step.beginDialog('/admin'); + + } else if (context.activity.text.startsWith('{"title"')) { + await step.beginDialog('/menu', JSON.parse(context.activity.text)); + + } else if ( + !(await this.deployer.getStoragePackageByName(min.instance.instanceId, `${min.instance.botId}.gbkb`)) && + process.env.GBKB_ENABLE_AUTO_PUBLISH === 'true' + ) { + await min.conversationalService.sendText(min, step, + `Oi, ainda não possuo pacotes de conhecimento publicados. Por favor, aguarde alguns segundos enquanto eu auto-publico alguns pacotes.` + ); + await step.beginDialog('/publish', { confirm: true, firstTime: true }); + } else { + + // Removes unwanted chars in input text. + + let text = context.activity.text; + const originalText = text; + text = text.replace(/<([^>]+?)([^>]*?)>(.*?)<\/\1>/gi, ''); + + // Saves special words (keep text) in tokens to prevent it from + // spell checking and translation. + + const keepText: string = min.core.getParam(min.instance, 'Keep Text', ''); + let keepTextList = []; + if (keepTextList) { + keepTextList = keepTextList.concat(keepText.split(';')); + } + const replacements = []; + await CollectionUtil.asyncForEach(min.appPackages, async (e: IGBPackage) => { + const result = await e.onExchangeData(min, 'getKeepText', {}); + if (result) { + keepTextList = keepTextList.concat(result); + } + }); + + const getNormalizedRegExp = (value) => { + var chars = [ + { letter: 'a', reg: '[aáàãäâ]' }, + { letter: 'e', reg: '[eéèëê]' }, + { letter: 'i', reg: '[iíìïî]' }, + { letter: 'o', reg: '[oóòõöô]' }, + { letter: 'u', reg: '[uúùüû]' }, + { letter: 'c', reg: '[cç]' } + ]; + + for (var i in chars) { + value = value.replace(new RegExp(chars[i].letter, 'gi'), chars[i].reg); + }; + return value; + }; + + let textProcessed = text; + if (keepTextList) { + keepTextList = keepTextList.filter(p => p.trim() !== ''); + let i = 0; + await CollectionUtil.asyncForEach(keepTextList, item => { + const it = GBConversationalService.removeDiacritics(item); + const noAccentText = GBConversationalService.removeDiacritics(textProcessed); + + if (noAccentText.toLowerCase().indexOf(it.toLowerCase()) != -1) { + const replacementToken = 'X' + GBAdminService.getNumberIdentifier().substr(0, 4); + replacements[i] = { text: item, replacementToken: replacementToken }; + i++; + textProcessed = textProcessed.replace(new RegExp(`\\b${getNormalizedRegExp(it.trim())}\\b`, 'gi'), `${replacementToken}`); + } + }); + } + + // Spells check the input text before translating, + // keeping fixed tokens as specified in Config. + + text = await min.conversationalService.spellCheck(min, textProcessed); + + // Detects user typed language and updates their locale profile if applies. + + let locale = min.core.getParam(min.instance, 'Default User Language', + GBConfigService.get('DEFAULT_USER_LANGUAGE') + ); + const detectLanguage = min.core.getParam(min.instance, 'Language Detector', + GBConfigService.getBoolean('LANGUAGE_DETECTOR') + ) === 'true'; + const systemUser = user.systemUser; + locale = systemUser.locale; + if (detectLanguage || !locale) { + locale = await min.conversationalService.getLanguage(min, text); + if (systemUser.locale != locale) { + + user.systemUser = await sec.updateUserLocale(systemUser.userId, locale); + await min.userProfile.set(step.context, user); + } + } + + // Checks for bad words on input text. + + const hasBadWord = wash.check(locale, context.activity.text); + if (hasBadWord) { + return await step.beginDialog('/pleaseNoBadWords'); + } + + // Translates text into content language, keeping + // reserved tokens specified in Config. + + const contentLocale = min.core.getParam( + min.instance, + 'Default Content Language', + GBConfigService.get('DEFAULT_CONTENT_LANGUAGE') + ); + text = await min.conversationalService.translate(min, text, contentLocale); + GBLog.info(`Translated text (processMessageActivity): ${text}.`); + + // Restores all token text back after spell checking and translation. + + if (keepTextList) { + let i = 0; + await CollectionUtil.asyncForEach(replacements, item => { + i++; + text = text.replace(new RegExp(`${item.replacementToken}`, 'gi'), item.text); + }); + } + step.context.activity['text'] = text; + step.context.activity['originalText'] = originalText; + + GBLog.info(`Final text ready for NLP/Search/.gbapp: ${text}.`); + + if (user.systemUser.agentMode === 'self') { + const manualUser = await sec.getUserFromAgentSystemId(user.systemUser.userSystemId); + + GBLog.info(`HUMAN AGENT (${user.systemUser.userSystemId}) TO USER ${manualUser.userSystemId}: ${text}`); + + const cmd = 'SEND FILE '; + if (text.startsWith(cmd)) { + const filename = text.substr(cmd.length); + const message = await min.kbService.getAnswerTextByMediaName(min.instance.instanceId, filename); + + if (message === null) { + GBLog.error(`File ${filename} not found in any .gbkb published. Check the name or publish again the associated .gbkb.`); + } else { + await min.conversationalService.sendMarkdownToMobile(min, null, manualUser.userSystemId, message); + } + } + else { + await min.whatsAppDirectLine.sendToDeviceEx(manualUser.userSystemId, `${manualUser.agentSystemId}: ${text}`, locale, + step.context.activity.conversation.id); + } + } + else { + + // If there is a dialog in course, continue to the next step. + + if (step.activeDialog !== undefined) { + await step.continueDialog(); + } else { + + const startDialog = user.hearOnDialog ? + user.hecallbackarOnDialog : + min.core.getParam(min.instance, 'Start Dialog', null); + + if (text !== startDialog) { + let nextDialog = null; + let data = { + query: text, + step: step, + notTranslatedQuery: originalText, + message: message ? message['dataValues'] : null, + user: user ? user.dataValues : null + }; + await CollectionUtil.asyncForEach(min.appPackages, async (e: IGBPackage) => { + if (!nextDialog) { + nextDialog = await e.onExchangeData(min, 'handleAnswer', data); + } + }); + data.step = null; + GBLog.info(`/answer being called from processMessageActivity (nextDialog=${nextDialog}).`); + await step.beginDialog(nextDialog ? nextDialog : '/answer', { + data: data, + query: text, + user: user ? user.dataValues : null, + message: message + }); + + } + } + } + } + } + } + \ No newline at end of file diff --git a/packages/customer-satisfaction.gbapp/dialogs/FeedbackDialog.ts b/packages/customer-satisfaction.gbapp/dialogs/FeedbackDialog.ts index 47425d08..a922f965 100644 --- a/packages/customer-satisfaction.gbapp/dialogs/FeedbackDialog.ts +++ b/packages/customer-satisfaction.gbapp/dialogs/FeedbackDialog.ts @@ -87,7 +87,7 @@ export class FeedbackDialog extends IGBDialog { let from = GBMinService.userMobile(step); await min.conversationalService.sendText(min, step, Messages[locale].please_wait_transfering); - const agentSystemId = await sec.assignHumanAgent(from, min.instance.instanceId); + const agentSystemId = await sec.assignHumanAgent(min, from); const user = await min.userProfile.get(step.context, {}); user.systemUser = await sec.getUserFromAgentSystemId(agentSystemId); diff --git a/packages/security.gbapp/services/SecService.ts b/packages/security.gbapp/services/SecService.ts index 435d2c72..fa9ad72b 100644 --- a/packages/security.gbapp/services/SecService.ts +++ b/packages/security.gbapp/services/SecService.ts @@ -2,7 +2,7 @@ const Fs = require('fs'); const urlJoin = require('url-join'); import { ConversationReference } from 'botbuilder'; -import { GBLog, GBService, IGBInstance } from 'botlib'; +import { GBLog, GBMinInstance, GBService, IGBInstance } from 'botlib'; import { CollectionUtil } from 'pragmatismo-io-framework'; import { GuaribasGroup, GuaribasUser, GuaribasUserGroup } from '../models'; import {FindOptions} from 'sequelize'; @@ -167,9 +167,19 @@ export class SecService extends GBService { return user.agentMode === 'self'; } - public async assignHumanAgent(userSystemId: string, instanceId: number): Promise { + public async assignHumanAgent(min: GBMinInstance, userSystemId: string): Promise { let agentSystemId; - const list = process.env.TRANSFER_TO.split(';'); + + let list = min.core.getParam( + min.instance, + 'Transfer To', + process.env.TRANSFER_TO + ); + + if (list){ + list = list.split(';') + } + await CollectionUtil.asyncForEach(list, async item => { if ( !(item !== undefined && @@ -181,7 +191,7 @@ export class SecService extends GBService { } }); GBLog.info(`Selected agentId: ${agentSystemId}`); - await this.updateHumanAgent(userSystemId, instanceId, agentSystemId); + await this.updateHumanAgent(userSystemId, min.instance.instanceId, agentSystemId); GBLog.info(`Updated agentId to: ${agentSystemId}`); return agentSystemId;