diff --git a/.gitignore b/.gitignore index d8b6f209..e9430015 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,5 @@ /packages/default.gbui/node_modules /tmp /work +/packages/default.gbdialog/bot.js +/packages/default.gbdialog/bot.ts diff --git a/package-lock.json b/package-lock.json index 10f5ac14..b7d49f64 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3299,9 +3299,9 @@ } }, "@types/node": { - "version": "9.6.36", - "resolved": "https://registry.npmjs.org/@types/node/-/node-9.6.36.tgz", - "integrity": "sha512-Fbw+AdRLL01vv7Rk7bYaNPecqmKoinJHGbpKnDpbUZmUj/0vj3nLqPQ4CNBzr3q2zso6Cq/4jHoCAdH78fvJrw==" + "version": "9.6.40", + "resolved": "https://registry.npmjs.org/@types/node/-/node-9.6.40.tgz", + "integrity": "sha512-M3HHoXXndsho/sTbQML2BJr7/uwNhMg8P0D4lb+UsM65JQZx268faiz9hKpY4FpocWqpwlLwa8vevw8hLtKjOw==" }, "async": { "version": "1.5.2", @@ -3548,9 +3548,9 @@ } }, "botlib": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/botlib/-/botlib-0.1.6.tgz", - "integrity": "sha512-NG/F7Yxhx/duehDzjI78mYMonZ03d+Gx+WtRmqj7TimGcc4xK1y4m7s+n9jt0XR+GYhj0jcA5uKA2LqDRtq22A==", + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/botlib/-/botlib-0.1.7.tgz", + "integrity": "sha512-vp8htUT/AL+pYXdiy9s13HFLbygCUorELw1dg1FEqHsfXQOoTlUvr52rNEeKikHvNYaXEEHqhv2F4pLRvEHIYw==", "requires": { "async": "2.6.1", "botbuilder": "4.1.3", @@ -3607,9 +3607,9 @@ } }, "@types/node": { - "version": "9.6.36", - "resolved": "https://registry.npmjs.org/@types/node/-/node-9.6.36.tgz", - "integrity": "sha512-Fbw+AdRLL01vv7Rk7bYaNPecqmKoinJHGbpKnDpbUZmUj/0vj3nLqPQ4CNBzr3q2zso6Cq/4jHoCAdH78fvJrw==" + "version": "9.6.40", + "resolved": "https://registry.npmjs.org/@types/node/-/node-9.6.40.tgz", + "integrity": "sha512-M3HHoXXndsho/sTbQML2BJr7/uwNhMg8P0D4lb+UsM65JQZx268faiz9hKpY4FpocWqpwlLwa8vevw8hLtKjOw==" }, "botbuilder": { "version": "4.1.3", diff --git a/package.json b/package.json index c1069077..6e49b5cf 100644 --- a/package.json +++ b/package.json @@ -66,7 +66,7 @@ "botbuilder-choices": "^4.0.0-preview1.2", "botbuilder-dialogs": "^4.1.5", "botbuilder-prompts": "^4.0.0-preview1.2", - "botlib": "0.1.6", + "botlib": "^0.1.7", "chai": "4.2.0", "child_process": "^1.0.2", "chokidar": "2.0.4", diff --git a/packages/core.gbapp/services/GBAPIService.ts b/packages/core.gbapp/services/GBAPIService.ts index 67637937..b494ea19 100644 --- a/packages/core.gbapp/services/GBAPIService.ts +++ b/packages/core.gbapp/services/GBAPIService.ts @@ -33,7 +33,9 @@ 'use strict'; import { TurnContext } from 'botbuilder'; +import { WaterfallStepContext } from 'botbuilder-dialogs'; import { GBMinInstance } from 'botlib'; +const WaitUntil = require('wait-until'); /** * @fileoverview General Bots server core. @@ -42,19 +44,21 @@ import { GBMinInstance } from 'botlib'; export class DialogClass { public min: GBMinInstance; public context: TurnContext; + public step: WaterfallStepContext; constructor(min: GBMinInstance) { this.min = min; - } - - public hear(text: string) { - // TODO: await this.context.beginDialog('textPrompt', text); } - public talk(text: string) { - this.context.sendActivity(text); + public async hear(cb) { + const id = Math.floor(Math.random() * 1000000000000); + this.min.cbMap[id] = cb; + await this.step.beginDialog('/feedback', { id: id }); } + public async talk(text: string) { + return await this.context.sendActivity(text); + } /** * Generic function to call any REST API. @@ -67,7 +71,5 @@ export class DialogClass { /** * Generic function to call any REST API. */ - public post(url: string, data) { - - } + public post(url: string, data) {} } diff --git a/packages/core.gbapp/services/GBCoreService.ts b/packages/core.gbapp/services/GBCoreService.ts index 4d0f4ca3..95b4cf86 100644 --- a/packages/core.gbapp/services/GBCoreService.ts +++ b/packages/core.gbapp/services/GBCoreService.ts @@ -184,7 +184,6 @@ export class GBCoreService implements IGBCoreService { alter: alter, force: force }); - } else { const msg = `Database synchronization is disabled.`; logger.info(msg); @@ -239,9 +238,15 @@ export class GBCoreService implements IGBCoreService { } public async ensureProxy(port): Promise { - const ngrok = require('ngrok'); - - return await ngrok.connect({ port: port }); + try { + const ngrok = require('ngrok'); + return await ngrok.connect({ port: port }); + } catch (error) { + // There are false positive from ngrok regarding to no memory, but it's just + // lack of connection. + logger.verbose(error); + throw new Error('Error connecting to remote ngrok server, please check network connection.'); + } } public async saveInstance(fullInstance: any) { @@ -313,7 +318,6 @@ export class GBCoreService implements IGBCoreService { } public loadSysPackages(core: GBCoreService) { - // NOTE: if there is any code before this line a semicolon // will be necessary before this line. // Loads all system packages. @@ -344,19 +348,19 @@ export class GBCoreService implements IGBCoreService { public async createBootInstance(core: GBCoreService, azureDeployer: AzureDeployerService, proxyAddress: string) { let instance: IGBInstance; - logger.info(`Deploying cognitive infrastructure (on the cloud / on premises)...`); - try { - instance = await azureDeployer.deployFarm(proxyAddress); - } catch (error) { - logger.warn( - `In case of error, please cleanup any infrastructure objects + logger.info(`Deploying cognitive infrastructure (on the cloud / on premises)...`); + try { + instance = await azureDeployer.deployFarm(proxyAddress); + } catch (error) { + logger.warn( + `In case of error, please cleanup any infrastructure objects created during this procedure and .env before running again.` - ); - throw error; - } - core.writeEnv(instance); - logger.info(`File .env written, starting General Bots...`); - GBConfigService.init(); + ); + throw error; + } + core.writeEnv(instance); + logger.info(`File .env written, starting General Bots...`); + GBConfigService.init(); return instance; } diff --git a/packages/core.gbapp/services/GBMinService.ts b/packages/core.gbapp/services/GBMinService.ts index 9266d40b..9e3ea2cf 100644 --- a/packages/core.gbapp/services/GBMinService.ts +++ b/packages/core.gbapp/services/GBMinService.ts @@ -43,21 +43,9 @@ const logger = require('../../../src/logger'); const request = require('request-promise-native'); const AuthenticationContext = require('adal-node').AuthenticationContext; -import { - AutoSaveStateMiddleware, - BotFrameworkAdapter, - ConversationState, - MemoryStorage, - UserState -} from 'botbuilder'; +import { AutoSaveStateMiddleware, BotFrameworkAdapter, ConversationState, MemoryStorage, UserState } from 'botbuilder'; -import { - GBMinInstance, - IGBAdminService, - IGBConversationalService, - IGBCoreService, - IGBPackage -} from 'botlib'; +import { GBMinInstance, IGBAdminService, IGBConversationalService, IGBCoreService, IGBPackage } from 'botlib'; import { GBAnalyticsPackage } from '../../analytics.gblib'; import { GBCorePackage } from '../../core.gbapp'; import { GBCustomerSatisfactionPackage } from '../../customer-satisfaction.gbapp'; @@ -117,10 +105,7 @@ export class GBMinService { // Serves default UI on root address '/'. const uiPackage = 'default.gbui'; - server.use( - '/', - express.static(UrlJoin(GBDeployer.deployFolder, uiPackage, 'build')) - ); + server.use('/', express.static(UrlJoin(GBDeployer.deployFolder, uiPackage, 'build'))); Promise.all( instances.map(async instance => { @@ -136,7 +121,7 @@ export class GBMinService { // Returns the instance object to clients requesting bot info. let botId = req.params.botId; - if (botId === '[default]'){ + if (botId === '[default]') { botId = bootInstance.botId; } @@ -171,15 +156,12 @@ export class GBMinService { // Build bot adapter. - const { min, adapter, conversationState } = await this.buildBotAdapter( - instance - ); + const { min, adapter, conversationState } = await this.buildBotAdapter(instance); // Install default VBA module. deployer.deployPackageFromLocalPath(min, 'packages/default.gbdialog'); - // Call the loadBot context.activity for all packages. this.invokeLoadBot(appPackages, min, server); @@ -188,32 +170,17 @@ export class GBMinService { const url = `/api/messages/${instance.botId}`; server.post(url, async (req, res) => { - return this.receiver( - adapter, - req, - res, - conversationState, - min, - instance, - appPackages - ); + return await this.receiver(adapter, req, res, conversationState, min, instance, appPackages); }); - logger.info( - `GeneralBots(${instance.engineName}) listening on: ${url}.` - ); + logger.info(`GeneralBots(${instance.engineName}) listening on: ${url}.`); // Serves individual URL for each bot user interface. const uiUrl = `/${instance.botId}`; - server.use( - uiUrl, - express.static(UrlJoin(GBDeployer.deployFolder, uiPackage, 'build')) - ); + server.use(uiUrl, express.static(UrlJoin(GBDeployer.deployFolder, uiPackage, 'build'))); logger.info(`Bot UI ${uiPackage} accessible at: ${uiUrl}.`); - const state = `${instance.instanceId}${Math.floor( - Math.random() * 1000000000 - )}`; + const state = `${instance.instanceId}${Math.floor(Math.random() * 1000000000)}`; // 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 @@ -226,9 +193,7 @@ export class GBMinService { ); authorizationUrl = `${authorizationUrl}?response_type=code&client_id=${ min.instance.authenticatorClientId - }&redirect_uri=${min.instance.botEndpoint}/${ - min.instance.botId - }/token`; + }&redirect_uri=${min.instance.botEndpoint}/${min.instance.botId}/token`; res.redirect(authorizationUrl); }); @@ -238,23 +203,16 @@ export class GBMinService { // access token that can be used to access the user owned resource. server.get(`/${min.instance.botId}/token`, async (req, res) => { - const state = await min.adminService.getValue( - min.instance.instanceId, - 'AntiCSRFAttackState' - ); + const state = await min.adminService.getValue(min.instance.instanceId, 'AntiCSRFAttackState'); if (req.query.state !== state) { - const msg = - 'WARNING: state field was not provided as anti-CSRF token'; + const msg = 'WARNING: state field was not provided as anti-CSRF token'; logger.error(msg); throw new Error(msg); } const authenticationContext = new AuthenticationContext( - UrlJoin( - min.instance.authenticatorAuthorityHostUrl, - min.instance.authenticatorTenant - ) + UrlJoin(min.instance.authenticatorAuthorityHostUrl, min.instance.authenticatorTenant) ); const resource = 'https://graph.microsoft.com'; @@ -271,47 +229,16 @@ export class GBMinService { logger.error(msg); res.send(msg); } else { - await this.adminService.setValue( - instance.instanceId, - 'refreshToken', - token.refreshToken - ); - await this.adminService.setValue( - instance.instanceId, - 'accessToken', - token.accessToken - ); - await this.adminService.setValue( - instance.instanceId, - 'expiresOn', - token.expiresOn.toString() - ); - await this.adminService.setValue( - instance.instanceId, - 'AntiCSRFAttackState', - null - ); + await this.adminService.setValue(instance.instanceId, 'refreshToken', token.refreshToken); + await this.adminService.setValue(instance.instanceId, 'accessToken', token.accessToken); + await this.adminService.setValue(instance.instanceId, 'expiresOn', token.expiresOn.toString()); + await this.adminService.setValue(instance.instanceId, 'AntiCSRFAttackState', null); res.redirect(min.instance.botEndpoint); } } ); }); - - // Setups handlers. - // send: function (context.activity, next) { - // logger.info( - // `[SND]: ChannelID: ${context.activity.address.channelId}, ConversationID: ${context.activity.address.conversation}, - // Type: ${context.activity.type} `) - // this.core.createMessage( - // this.min.conversation, - // this.min.conversation.startedBy, - // context.activity.source, - // (data, err) => { - // logger.info(context.activity.source) - // } - // ) - // next() }) ); } @@ -388,8 +315,10 @@ export class GBMinService { min.conversationalService = this.conversationalService; min.adminService = this.adminService; min.instance = await this.core.loadInstance(min.botId); + min.userProfile = conversationState.createProperty('userProfile'); const dialogState = conversationState.createProperty('dialogState'); + min.dialogs = new DialogSet(dialogState); min.dialogs.add(new TextPrompt('textPrompt')); @@ -417,18 +346,18 @@ export class GBMinService { p.channel.received(req, res); }); } - }, this); + }, this); appPackages.forEach(e => { e.sysPackages = sysPackages; e.loadBot(min); - }, this); + }, this); } /** * Bot Service hook method. */ - private receiver( + private async receiver( adapter: BotFrameworkAdapter, req: any, res: any, @@ -437,8 +366,9 @@ export class GBMinService { instance: any, appPackages: any[] ) { - return adapter.processActivity(req, res, async context => { - const state = conversationState.get(context); + return await adapter.processActivity(req, res, async context => { + // Get loaded user state + const state = await conversationState.get(context); const step = await min.dialogs.createContext(context, state); step.context.activity.locale = 'en-US'; // TODO: Make dynamic. @@ -454,18 +384,16 @@ export class GBMinService { }); user.loaded = true; user.subjects = []; + user.cb = null; await min.userProfile.set(step.context, user); } logger.info( - `User>: ${context.activity.text} (${context.activity.type}, ${ - context.activity.name - }, ${context.activity.channelId}, {context.activity.value})` + `User>: ${context.activity.text} (${context.activity.type}, ${context.activity.name}, ${ + context.activity.channelId + }, {context.activity.value})` ); - if ( - context.activity.type === 'conversationUpdate' && - context.activity.membersAdded.length > 0 - ) { + if (context.activity.type === 'conversationUpdate' && context.activity.membersAdded.length > 0) { const member = context.activity.membersAdded[0]; if (member.name === 'GeneralBots') { logger.info(`Bot added to conversation, starting chat...`); @@ -480,12 +408,15 @@ export class GBMinService { } // Processes messages. + } else if (context.activity.type === 'message') { // Checks for /admin request. if (context.activity.text === 'vba') { min.sandbox.context = context; + min.sandbox.step = step; min.sandbox['bot'].bind(min.sandbox); - min.sandbox['bot'](); + + await min.sandbox['bot'](); } else if (context.activity.text === 'admin') { await step.beginDialog('/admin'); @@ -497,7 +428,9 @@ export class GBMinService { // Otherwise, continue to the active dialog in the stack. } else { - if (step.activeDialog) { + const user = await min.userProfile.get(context, {}); + + if (step.activeDialog || user.dialog) { await step.continueDialog(); } else { await step.beginDialog('/answer', { @@ -508,7 +441,6 @@ export class GBMinService { // Processes events. } else if (context.activity.type === 'event') { - // Empties dialog stack before going to the target. await step.endAll(); @@ -539,13 +471,12 @@ export class GBMinService { await step.continueDialog(); } } + await conversationState.saveChanges(context, true); } catch (error) { const msg = `ERROR: ${error.message} ${error.stack ? error.stack : ''}`; logger.error(msg); - await step.context.sendActivity( - Messages[step.context.activity.locale].very_sorry_about_error - ); + await step.context.sendActivity(Messages[step.context.activity.locale].very_sorry_about_error); await step.beginDialog('/ask', { isReturning: true }); } }); diff --git a/packages/core.gbapp/services/GBVMService.ts b/packages/core.gbapp/services/GBVMService.ts index bc8e096b..d4e45bae 100644 --- a/packages/core.gbapp/services/GBVMService.ts +++ b/packages/core.gbapp/services/GBVMService.ts @@ -76,13 +76,15 @@ export class GBVMService implements IGBCoreService { // Convert TS into JS. const tsfile = `bot.ts`; const tsc = new TSCompiler(); - tsc.compile([UrlJoin(path, tsfile)]); + // TODO: tsc.compile([UrlJoin(path, tsfile)]); // Run JS into the GB context. const jsfile = `bot.js`; localPath = UrlJoin(path, jsfile); if (fs.existsSync(localPath)) { let code: string = fs.readFileSync(localPath, 'utf8'); code = code.replace(/^.*exports.*$/gm, ''); + code = code.replace(/this\./gm, 'await this.'); + code = code.replace(/function/gm, 'async function'); const sandbox: DialogClass = new DialogClass(min); const context = vm.createContext(sandbox); vm.runInContext(code, context); diff --git a/packages/customer-satisfaction.gbapp/dialogs/FeedbackDialog.ts b/packages/customer-satisfaction.gbapp/dialogs/FeedbackDialog.ts index ca62c7fc..7074ed04 100644 --- a/packages/customer-satisfaction.gbapp/dialogs/FeedbackDialog.ts +++ b/packages/customer-satisfaction.gbapp/dialogs/FeedbackDialog.ts @@ -78,35 +78,48 @@ export class FeedbackDialog extends IGBDialog { ]) ); - min.dialogs.add(new WaterfallDialog('/feedback', [ - async step => { - const locale = step.context.activity.locale; - if (step.result.fromMenu) { + min.dialogs.add( + new WaterfallDialog('/feedback', [ + async step => { + const locale = step.context.activity.locale; + await step.context.sendActivity(Messages[locale].about_suggestions); + step.activeDialog.state.cbId = step.options['id']; + + return await step.prompt('textPrompt', Messages[locale].what_about_service); + }, + async step => { + + console.log(step.result); + + // min.sandbox.context = step.context; + // min.sandbox.step = step; + + let cbId = step.activeDialog.state.cbId; + let cb = min.cbMap[cbId]; + cb.bind({ step: step, context: step.context }); + await cb(); + + // const locale = step.context.activity.locale; + // const rate = await AzureText.getSentiment( + // min.instance.textAnalyticsKey, + // min.instance.textAnalyticsEndpoint, + // min.conversationalService.getCurrentLanguage(step), + // step.result + // ); + + // if (rate > 0.5) { + // await step.context.sendActivity(Messages[locale].glad_you_liked); + // } else { + // await step.context.sendActivity(Messages[locale].we_will_improve); + + // // TODO: Record. + // } + // await step.replaceDialog('/ask', { isReturning: true }); + + return await step.next(); } - - await step.prompt('textPrompt', Messages[locale].what_about_service); - return await step.next(); - }, - async step => { - const locale = step.context.activity.locale; - const rate = await AzureText.getSentiment( - min.instance.textAnalyticsKey, - min.instance.textAnalyticsEndpoint, - min.conversationalService.getCurrentLanguage(step), - step.result - ); - - if (rate > 0.5) { - await step.context.sendActivity(Messages[locale].glad_you_liked); - } else { - await step.context.sendActivity(Messages[locale].we_will_improve); - - // TODO: Record. - } - await step.replaceDialog('/ask', { isReturning: true }); - return await step.next(); - } - ])); + ]) + ); } } diff --git a/packages/default.gbdialog/bot.vbs b/packages/default.gbdialog/bot.vbs index c902c27b..ffb1425a 100644 --- a/packages/default.gbdialog/bot.vbs +++ b/packages/default.gbdialog/bot.vbs @@ -35,5 +35,4 @@ this.talk ("Please, what's your e-mail address?") let email = this.hear() this.talk("Thanks, sending e-mail to: " + email); this.sendEmail(email, "Message from VBA Bot", "Yes, I can send e-mails."); - %> \ No newline at end of file diff --git a/src/app.ts b/src/app.ts index fb2b1afb..01a438f7 100644 --- a/src/app.ts +++ b/src/app.ts @@ -93,6 +93,7 @@ export class GBServer { // Ensures cloud / on-premises infrastructure is setup. logger.info(`Establishing a development local proxy (ngrok)...`); + const proxyAddress: string = await core.ensureProxy(port); logger.info(`Deploying packages...`);